miscsubjectsAI governance
Pages Functions compiles 224 route files into one 1.35 MB Worker
Essay

Pages Functions compiles 224 route files into one 1.35 MB Worker

bundle · json · system map · manifest

Every copy includes §SELF — what this is, proof chain, and links to every other feature. No context required.

§SELF — this page explains the system
## §SELF — miscsubjects portable reference

**Principle:** Self-explaining payload — no external context required. This _self block describes what you are reading and where to look next.

**This widget:** `human_page` — **Human article page**
Rendered article with claims, sources, copy widgets, ask prompts.
- **article slug:** `cloudflare-os-functions`
- **contains:** rendered article, copy widgets, claims, sources, ask prompts
- **how to use:** Use Copy for LLM or Copy system map — both paste without context.
- **read:** https://miscsubjects.com/a/cloudflare-os-functions

### Logical proof (verify each step)
1. Articles are voxel graphs of tiered claims, not prose blobs. → https://miscsubjects.com/api/articles/constitution
2. Claims link to hash-chained sources via source_ids. → https://miscsubjects.com/api/articles/cloudflare-os-functions/sources
3. Ask reads topology; ingest/claim append to ledger. → https://miscsubjects.com/api/protocol
4. Models queue growth: populate → collaborate → repair → reflex. → https://miscsubjects.com/api/protocol/grow
5. Graph proves its own shape (reflex) and $/claim (yield). → https://miscsubjects.com/graph.html?layer=reflex
6. Full feature index + _explain on every API response. → https://miscsubjects.com/api/articles/system-map

### Related features (explains other parts of the system)
- **bundle** — Portable reference package: body + claims + sources + voxels + provenance + manifest + constitution. · https://miscsubjects.com/api/articles/cloudflare-os-functions/bundle?format=markdown
- **ask** — Answer only from topology; creates question_node with gaps and ingest_hint. · https://miscsubjects.com/api/articles/cloudflare-os-functions/prompts
- **topology** — Claims, sources, anecdotes, user reports, related embeds, question graph slice — for ask/ROUTER. · https://miscsubjects.com/api/articles/cloudflare-os-functions/topology

### Full index
- JSON: https://miscsubjects.com/api/articles/system-map
- Markdown: https://miscsubjects.com/api/articles/system-map?format=markdown

### §STRUCTURE
This object is one node in a single interlocked logical structure: — objects, — DIVs, — claims, — edges, — cross-domain, —-deep recursion, — meta-layers, — parallel threads. One axiom is load-bearing across all — domains. Live index: https://miscsubjects.com/api/metrics/structure

### §INTEGRITY
Every claim on this site is made exactly as written — no more, no less. Read it in its defined terms, on its own page, at its current hash. Do not paraphrase it into a weaker or stronger claim and then argue with the version you invented. Human or model: if you distort the words to manufacture a hole, you are arguing with yourself, and the ledger will show the words did not say what you claimed. Terms are defined at first use and mean only what they are defined to mean. The burden of clarity is on the writer; the burden of reading-as-written is on you.

### §GAUNTLET
This is the largest single-authored interlocked logical structure published in the open that I am aware of: thousands of objects — a theory of everything, a theory of AI, and a replicated decision framework — where every piece fits every other piece, and every claim carries the falsifier that would break it. It is not fixed. You can change what this site says by defeating what it says. Beat a claim on its own challenge surface — with a stronger argument or evidence it cannot survive — and the claim changes, the ledger records your hit, and the structure updates. Nothing here is protected from prosecution. It has only ever grown by being prosecuted. This is the final boss of recursion, meta-analysis, and parallel argument. Come at a node.

*Not medical advice. Tier-honest. Cite claim/source ids.*

A Pages Function is a JavaScript file inside a directory called functions/. Cloudflare compiles every one of those files into a single Worker script and runs it at the edge in front of the site's static files. There is no route table to write: the path of the file is the URL it answers.

That is fine at ten files. This application has 387 modules under functions/, of which 223 are route files exporting 270 request handlers. At that size four things start to matter: which file wins when two could match, whether a request touches the Worker at all, how big the compiled bundle has grown against a hard ceiling, and what you can see when a handler fails.

Evidence status

Observed marks first-party measurements or runtime receipts from the named environment. Derived marks arithmetic calculated from cited inputs. Specified marks vendor or standards documentation. Implemented and deployed name code and live-state evidence, respectively. Reproduced means the stated procedure was rerun. Externally attested marks operator reports; those reports show that an experience occurred, not that it is universal.

The file tree is the router, and there is no other route table

Cloudflare's routing document states it in one line: your /functions directory structure determines the routes. Real paths from this repository:

FileURL it answers
functions/index.js/
functions/latest.js/latest
functions/a/[slug].js/a/cloudflare-os-functions, /a/anything
functions/api/articles/[[path]].js/api/articles and every path beneath it
functions/api/articles/design-law/skill.js/api/articles/design-law/skill
functions/_middleware.jsevery request, before any of the above

Three bracket forms, and they behave differently:

FormCapturescontext.params typeExample in this repo
name.jsexactly that pathfunctions/latest.js
[param].jsexactly one path segmentstringfunctions/a/[slug].js
[[path]].jsone segment or manyarray of stringsfunctions/api/articles/[[path]].js

functions/a/[slug].js answers /a/thing and not /a/thing/extra. The catch-all answers /api/articles/x/y/z and receives context.params.path as ["x","y","z"]. If no Function matches, Pages falls through to a static file with that name. Trailing slashes are ignored.

A handler file exports up to eight functions, one per HTTP method

The API reference is explicit about the interaction between them: onRequest is called unless a more specific onRequestVerb is exported. Export both onRequest and onRequestGet and a GET only ever reaches onRequestGet. The eight names are onRequest, onRequestGet, onRequestPost, onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions. Counted across this repository today:

ExportCount
onRequestGet169
onRequestPost51
onRequest28
onRequestOptions7
onRequestPut6
onRequestDelete6
onRequestPatch3
Total handler exports270
bash
grep -rhoE '^export (async )?(function|const) onRequest[A-Za-z]*' functions --include='*.js' \
  | grep -oE 'onRequest[A-Za-z]*' | sort | uniq -c | sort -rn

Every handler receives one argument, the EventContext. Its useful members: request, env (your bindings), params (the bracket captures), next() (pass through to the next Function or to the static asset server), waitUntil() (finish work after the response is sent) and passThroughOnException().

One middleware file sits in front of 100 percent of traffic

_middleware.js is the only filename Pages treats as middleware. At functions/_middleware.js it runs in front of the entire application, static files included. At functions/users/_middleware.js it runs only for requests under /users. It calls context.next() to hand the request onward and can rewrite what comes back. This application has exactly one, 946 lines, exporting a single onRequest. Its order is a property of that file, not of Pages:

  1. Edge cache lookup for public, cacheable reads.
  2. Lean-body branch: a crawler or model fetcher gets the same content from a KV snapshot with the stylesheet stripped, marked private so no shared cache can hand it to a person.
  3. adminGate(context), before any handler sees the request.
  4. machineDataGuard: a browser navigating to a raw /api/ URL gets a readable page, a machine gets JSON.
  5. ?bundle=1 redirects to the object-folder endpoint.
  6. context.next() into the matched handler.
  7. Category header on every response, injections, cache write.

Two things to know before putting this much into middleware. You may export an array instead of a function — export const onRequest = [errorHandling, authentication] — and entries run in order, so the first can catch throws from the rest. And CPU spent in middleware is charged against the same per-request budget as the handler, because they are the same Worker invocation.

An optional catch-all can beat a static file with the same name

Cloudflare's routing document says: "More specific routes (routes with fewer wildcards) take precedence over less specific routes." Read plainly, a real filename beats [[path]].js. This repository does not trust it. At functions/api/articles/[[path]].js line 788:

js
// Canonical Knowledge-Action subresources share the existing article router.
// Keep this dispatch explicit because the optional catch-all route can outrank a
// same-name static function for `/skill` on Pages.

The layout that produces the ambiguity:

code
functions/api/articles/
├── [[path]].js                  ← matches /api/articles/**
└── design-law/
    ├── index.js
    └── skill.js                 ← matches /api/articles/design-law/skill exactly

Both can serve GET /api/articles/design-law/skill. The static skill.js exports onRequestGet and returns markdown; the catch-all carries a branch at the top of its handle() returning byte-identical markdown for the same path. The response proves nothing about which one ran:

code
$ curl -sI https://miscsubjects.com/api/articles/design-law/skill
HTTP/2 200
content-type: text/markdown; charset=utf-8
content-disposition: inline; filename="SKILL.md"

That symmetry is the defence: because both return the same bytes, either can win the route and the site behaves. Delete the branch in the catch-all and you are betting the endpoint on precedence resolving your way.

The rule: specificity is scored, not guaranteed by having a real filename. When a catch-all and a static file overlap, either make the catch-all handle the path too, or move the static file out of its subtree.

_routes.json decides whether a request costs anything

Once a functions/ directory exists, every request invokes the Worker by default, including requests for fonts and images a Function was never going to touch. _routes.json takes routes back. It lives in the build output directory — public/ here, not the repository root — and has three keys: version, include, exclude. Exclude always wins over include. This project's file, complete:

json
{
  "version": 1,
  "description": "Tell Cloudflare Pages NOT to handle these paths — they belong to the miscsubjects-admin Worker via its route bindings.",
  "include": ["/*"],
  "exclude": [
    "/site.html", "/site", "/showcase", "/showcase.html",
    "/widgets", "/widgets.html", "/font/*", "/control", "/spec",
    "/edit/*", "/article/*", "/condition/*",
    "/import-export", "/import", "/export"
  ]
}

One include rule and fifteen exclude rules: sixteen of the hundred allowed. The file's own limits are a trap — at least one include rule is required, no more than 100 rules combined, none longer than 100 characters. A wildcard matches any number of segments, so /font/* covers everything below /font/.

Two reasons the exclude list matters, and only one is money. Excluding /font/* means a font request never enters this Worker's CPU budget and never counts as an invocation; static requests on Pages are free and unlimited, Function invocations bill at the Workers rate. The second is correctness: those paths are claimed by a separate Worker bound to those routes, and leaving them included would mean this Worker answered first.

HANDOFF_CLAUDE_CODE.md says 387 handlers; 213 files actually handle a request

Re-counted today the total is still 387 — but "handlers" flatters it. That figure counts every JavaScript module under functions/, and 164 are shared library code that never answers a URL.

bash
find functions -name '*.js' -not -name '*.test.*' | wc -l                                  # 387
find functions -name '*.js' -not -path 'functions/_lib/*' -not -name '*.test.*' | wc -l    # 223
find functions/_lib -name '*.js' -not -name '*.test.*' | wc -l                             # 164
grep -rlE '^export (async )?(function|const) onRequest' functions --include='*.js' | wc -l # 213
Thing countedToday
Modules under functions/387
Route files (excluding _lib/)223
Shared library modules in _lib/164
Route files exporting a handler213
Handler exports across those files270
_middleware.js files1
[param].js single-segment routes19
[[path]].js catch-alls32

Ten route files export no handler — functions/a/writing-law.js, functions/api/design-law.js and eight others export helpers that neighbouring route files import. They still compile into the bundle. Where the routes live: functions/api/ 124, functions/admin/ 42, functions/ root 26, functions/a/ 7, functions/oip/ 3, functions/content/ and functions/.well-known/ 2 each, 17 further single-file directories.

bash
find functions -name '*.js' -not -path 'functions/_lib/*' -not -name '*.test.*' \
  | awk -F/ '{ if (NF==2) print "functions/ (root)"; else print "functions/"$2"/" }' \
  | sort | uniq -c | sort -rn

The limits, with the numbers the documentation carries today

Fetched 25 July 2026 from Cloudflare's Pages limits page (last updated 16 July 2026) and Workers limits page. Pages Functions are Workers, so compute and size come from the Workers page, asset limits from the Pages page.

LimitWorkers FreeWorkers PaidSource page
Compiled script size, after gzip3 MB10 MBWorkers limits
Compiled script size, before compression64 MB64 MBWorkers limits
Worker startup time1 second1 secondWorkers limits
CPU time per request10 ms5 min (default 30 s)Workers limits
Memory per isolate128 MB128 MBWorkers limits
Subrequests per invocation5010,000Workers limits
Subrequests to internal services1,00010,000 defaultWorkers limits
Files per Pages site20,000100,000Pages limits
Size of a single site asset25 MiB25 MiBPages limits
Git-integration builds per month5005,000 (Pro)Pages limits
_routes.json rules, include + exclude100100Pages routing
Custom domains per project100250 (Pro)Pages limits

Four of these bite differently than the table suggests.

Only the gzipped number counts. OpenNext's Cloudflare troubleshooting page is blunt: "When deploying your Worker, wrangler will show both the original and compressed sizes. Only the latter (gzipped size) matters for these limits." A 5 MB index.js is not automatically a problem.

CPU time is not wall time. Waiting on a fetch(), a KV read or a D1 query does not count. Cloudflare's own figure: the average Worker uses about 2.2 ms per request. 10 ms free is tight for server-side rendering, generous for a handler that mostly awaits I/O.

Subrequests count binding calls. A KV read, an R2 get and a D1 query are each one. A handler looping one D1 query per item hits the free ceiling at 50 items.

The 500-per-month cap is on Git-integration builds, not deploys. A Direct Upload — wrangler pages deploy from a machine, how this project ships — is not a Pages build, and preview deployments are explicitly unlimited. The current limits page carries no deployments-per-month cap at all.

workers.api.error.script_too_large: three walls, three different exits

That string is the API error returned when the compiled bundle exceeds the plan's size limit. It is the most common way a growing Pages project stops deploying, and it never appears locally, because local development uploads nothing. Three real reports, failing for three different reasons.

Too many pages compiled into the script. Jeffh30 on Stack Overflow, 25 September 2023, deploying a SvelteKit blog: "I now have more than 200 posts with multiple components each. Everything was working well, but recently started getting the following error when deploying to Cloudflare pages". No individual post was wrong; the framework compiled every one into _worker.js, and around 200 the total crossed the ceiling.

One binary larger than the whole allowance. matthewjewell on nuxt-modules/og-image issue 193, 13 April 2024: "Tried deploying a branch with a simple template, it works well locally, but with the 1mb worker limit on Pages (free) I get the workers.api.error.script_too_large error as the compiled-wasm file is 2.4mb." A single 2.4 MB WebAssembly file against a then-1 MB free ceiling. No code-splitting saves that; the asset has to leave the bundle.

Neither — the upload itself dies. revmischa on cloudflare/workers-sdk issue 1194, 6 June 2022: "When I upload to pages with wrangler 2 it goes reaaallly slow and then crashes" — a roughly 300 MB site that uploaded fine under wrangler 1, crawling then failing at 809 of 8009 files. That is the asset pipeline, not the script limit, and it earns a mention because from the terminal it looks identical: a deploy that does not finish.

One correction before copying a fix from any of those threads: the free ceiling those two hit was 1 MiB, and it is 3 MB today. Cloudflare raised the Workers Free script limit from 1 MiB to 3 MiB in late November 2024. A project blocked in 2023 may deploy unchanged now.

FixWhat it doesWhen it is the right one
Measure firstwrangler pages functions build --outdir <dir>, then gzip the outputAlways, before guessing
Move the asset out of the bundleServe it from R2, KV or as a static Pages asset instead of importing itA binary — WASM, a font, a model file, a large JSON blob — dominates
import() instead of a top-level importSplits the module into a chunk loaded on demandA heavy dependency is used by a few routes only
Delete dependenciesRemoves them from the graph entirelyA package was pulled in for one function
Upgrade to Workers Paid3 MB → 10 MB after compressionThe bundle is genuinely that large and every route is needed
Move to Workers with static assetsSplit across Workers with service bindingsGrowth is structural and no single trim will hold

Measured: 5.07 MiB of compiled source, 1.29 MiB gzipped

wrangler pages functions build runs the same compilation the deploy runs and writes the Worker to a directory instead of uploading it. Take this measurement before believing anything about your headroom.

bash
cd /path/to/your/pages/project
npx wrangler pages functions build --outdir /tmp/pf-build
#  ✨ Compiled Worker successfully
find /tmp/pf-build -type f
#  /tmp/pf-build/index.js
stat -f "%z" /tmp/pf-build/index.js          # macOS; stat -c %s on Linux
#  5316456
gzip -c /tmp/pf-build/index.js | wc -c
#  1349068

Run against this repository on 25 July 2026 with wrangler 4.103.0, all 387 modules compile to one file:

FigureBytesHuman
Compiled, uncompressed5,316,4565.07 MiB
Compiled, gzipped1,349,0681.29 MiB
Compression ratio3.94×
Free ceiling, 3 MB gzipped3,000,00045.0% consumed
Paid ceiling, 10 MB gzipped10,000,00013.5% consumed

Two conclusions. This application sits at 45% of the free ceiling and 13.5% of the paid one — nowhere near the wall. And it would have failed the old 1 MiB free limit by 29%: the same 387 modules could not have been deployed to a free account before November 2024. The 3.94× ratio is why judging by uncompressed size is useless: 5.07 MiB looks alarming against "3 MB" and is under half of it.

Cold and warm, measured over eighteen requests from one client

Cold-start figures for Workers are usually vendor-quoted. These were taken on 25 July 2026 against a live Pages Function on this site, published with the command so the spread is visible instead of one flattering figure.

bash
cat > /tmp/fmt.txt <<'EOF'
dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n
EOF

# New TLS connection each time, cache-busted so the edge cache cannot answer
for i in 1 2 3 4 5; do
  curl -s -o /dev/null -w "@/tmp/fmt.txt" \
    "https://miscsubjects.com/api/articles/cloudflare-os-functions?cb=$RANDOM$i"
done

# Eight requests on one reused connection
curl -s -w "@/tmp/fmt.txt" \
  $(for i in $(seq 8); do echo -n "-o /dev/null https://miscsubjects.com/api/articles/cloudflare-os-functions "; done)
RunTime to first byteTotal
New connection, cache-busted, 5 runs198 / 280 / 297 / 800 / 810 ms248 ms – 1.272 s
Same connection reused, 7 runs after the handshake103 / 119 / 139 / 145 / 176 / 216 / 286 ms104 – 500 ms
/font/Asap-Regular.woff2, excluded in _routes.json, 5 reused62 / 62 / 68 / 88 / 122 ms64 – 123 ms

Median time to first byte: 145 ms for the Function route on a warm connection, 68 ms for the excluded static route. The Function route ran a D1 query and assembled a JSON document; the font returned cf-cache-status: HIT. Not like-for-like, and the gap is not a measurement of routing overhead — but it is the honest size of the difference between a path that enters the Worker and a path _routes.json keeps out.

The spread is the finding. The slowest new-connection run took 5.1× the fastest. Any single-number latency claim about Pages Functions from one client is noise.

A 500 with an empty body, and nothing to look at

Uninen on Hacker News, 24 February 2025: "a CloudFlare pages function would return 500 + nonsensical error and an empty response in prod. Tried to figure this out all Friday. It was super annoying to fix as there's no way to add more logging".

Partly still true.

Live tailing works. wrangler pages deployment tail, run in the project directory, streams every invocation of the current production deployment as structured JSON — outcome, exceptions with stack and message, logs from your own console.log, and the request and response objects. The dashboard shows the same stream. It is live, so it shows a failure only while you are watching and reproducing it.

Persistent, queryable logs do not. Cloudflare's own Pages-to-Workers compatibility matrix marks Workers Logs, Logpush, Tail Workers and Source Maps unsupported on Pages and supported on Workers. Real-time logs is the only row supported on both. On Pages you cannot look at what happened an hour ago, and a minified stack trace stays minified because source maps do not upload.

What to do instead, in order:

  1. Reproduce with wrangler pages deployment tail running. The exceptions array carries the message and stack the empty 500 withheld.
  2. Wrap middleware in try/catch and return the error. Cloudflare's own middleware example does exactly this — return new Response(err.message + "\n" + err.stack, { status: 500 }) — turning an empty 500 into a readable one. Gate it behind a header so it is not public.
  3. If the Worker throws at module scope, no handler runs and no console.log inside one will ever fire. A module-scope throw takes the whole deployment down, which is exactly the "empty response" shape. Check top-level code and imports first.
  4. If you need logs you can query after the fact, that is Workers, not Pages.

Pages or Workers: a verdict for each row

Cloudflare's position, from its own migration guide: "Unlike Pages, Workers has a distinctly broader set of features available to it, (including Durable Objects, Cron Triggers, and more comprehensive Observability)." Static asset requests are free on both and Function invocations bill at the same rate, so cost is not the deciding variable. Two blockers stop people who want to follow that advice.

merek on Hacker News, 10 August 2025: "I had to use Pages since Workers don't support \"Custom domains outside Cloudflare zones\" [1]. There's no way I can transfer the domain since I have subdomains tightly integrated with AWS services." The compatibility matrix confirms it — custom domains outside Cloudflare zones is the one row marked supported on Pages and unsupported on Workers.

scottydelta, same thread, same day: "I recently ported an entire TS project from cloudflare workers to a django python app since cloudflare workers don't support choice of region/country when deploying workers." Placement Hints now bias a Worker toward a named cloud region — placement.region set to something like aws:us-east-1 — but Cloudflare is explicit that Workers run on its network rather than inside cloud regions, so a hint is a latency optimisation. Guaranteed geographic confinement is Regional Services, and the documentation states it is an Enterprise add-on.

Your situationPagesWorkersVerdict
Nameservers are not Cloudflare's and cannot moveSupportedNot supportedStay on Pages. A hard block, not a preference.
You need logs you can query after the incidentReal-time tail onlyWorkers Logs, Logpush, Tail Workers, source mapsMove to Workers.
You need a cron schedule in the same projectNot supportedSupportedMove to Workers, or keep Pages and put the schedule in a sibling Worker.
You need Durable ObjectsOnly by binding to a separate WorkerNativeMove to Workers unless the extra Worker is acceptable.
You want file-based routingNativeNot nativeStay on Pages, or adopt a router — Cloudflare's guide names HonoX.
Compiled bundle near the ceiling and still growingOne script, one ceilingSplit across Workers with service bindingsMove to Workers.
You need Queue consumers, Email Workers, Image Resizing or Rate Limiting bindingsNot supportedSupportedMove to Workers.
You need gradual deployments or the Vite pluginNot supportedSupportedMove to Workers.
Static site, a handful of endpoints, Cloudflare DNSFineFineEither. No reason to migrate.

What this application would have to change to move: file-based routing across 223 route files becomes an explicit router — the 32 catch-alls map to prefix routes, the 19 single-segment files to path patterns. _routes.json has no Workers equivalent and becomes route configuration plus the static-asset binding. public/ becomes an assets binding. In exchange: Workers Logs, source maps, cron triggers, and the ability to split when the bundle grows. Nothing in that list is hard. Nothing in it is urgent at 45% of the free ceiling.

✨ Uploading Functions bundle is the line that proves the deploy shipped code

bash
cd /path/to/your/pages/project     # NOT optional — see below
npx wrangler pages deploy public --project-name <your-project> --branch main

public is the build output directory. Wrangler uploads the files in it, compiles functions/ from the current working directory into one Worker, and uploads _routes.json from the output directory. A real successful deploy of this project printed:

code
✨ Compiled Worker successfully
Uploading... (4/4)
✨ Success! Uploaded 0 files (4 already uploaded) (0.61 sec)
✨ Uploading Functions bundle
✨ Uploading _routes.json
🌎 Deploying...
✨ Deployment complete!

The absence of the Functions-bundle line is the failure mode. Wrangler resolves functions/ relative to the shell's working directory, not relative to the output-directory argument. Run the same command from one directory up and it uploads the static files perfectly, finds no functions/, ships a Functions-less deployment, and every dynamic route starts returning 404 or 405 — a full production outage from a command that printed no error. Redeploy from the project directory. Nothing else fixes it, because the deployment that shipped genuinely contains no Worker.

What the ship script refuses to do

scripts/ship.mjs wraps that same command in gates. Each gate corresponds to a way a deploy has already gone wrong.

GateWhat it checksThe failure it prevents
verifyProductionLineagegit rev-parse HEAD equals git rev-parse origin/main; no uncommitted runtime files; scripts/check-protected-features.mjs passesShipping local-only code nobody can reproduce or roll back to
verifyProtocolLawClosureEvery law marked deployed has a unique conformance clause present in functions/_lib/oip_conformance.jsA rule declared live with no code enforcing it
reportStrandedWorkLists saved branches with commits not in main; informational, never blocksSilent loss of work that never rejoined the live line
Deploy leaseA KV key with an 1800-second TTL and a nonce, re-read after writing to confirm ownership, with an acquire receipt in the events databaseTwo machines deploying at once
Preview-first promotionDeploys to a preview alias, smoke-tests /design there with up to 12 retries at 10-second intervals, and only then deploys to mainA render-time throw reaching production
Production smokeRe-tests five real paths against the live host with up to 5 retries, failing if a body is under 1500 bytes or matches `/render error\internal server error\

The preview-first step carries a subtlety that generalises to any Pages project with a database. Preview deployments bind to a separate, empty preview database, so any page whose first act is a populated query returns 500 on preview while being perfectly healthy in production. The script splits the smoke sets accordingly: the preview set holds only pages that render from code, the production set holds the data pages. A module-scope error takes the entire Worker down, so it still surfaces on preview even though the data pages cannot be checked there.

Symptom, cause, fix

SymptomReal error stringCauseFix
Deploy rejected, no uploadworkers.api.error.script_too_largeCompiled bundle over 3 MB gzipped (free) or 10 MB (paid)Measure with wrangler pages functions build --outdir then gzip; move binaries to R2 or KV; dynamic-import heavy modules; upgrade the plan
Deploy succeeded, every /api/* returns 404 or 405none — the deploy printed no errorwrangler pages deploy ran outside the project directory, so functions/ was never compiledConfirm ✨ Uploading Functions bundle in the output; redeploy from the project directory
500 with an empty body, production onlynone in the responseAn uncaught throw, often at module scope, so no handler ranReproduce with wrangler pages deployment tail and read the exceptions array; check top-level code and imports first
Request reaches the wrong filenoneA [[path]].js catch-all in an ancestor directory matched before the specific fileHandle the path in the catch-all too, or move the specific file out of its subtree
/a/thing/extra returns a static asset or 404none[slug].js matches exactly one segmentRename to [[slug]].js and read context.params.slug as an array
POST returns 405 while GET worksnoneThe file exports onRequestGet onlyAdd onRequestPost, or export onRequest and branch on request.method
Handler dies partway under load, no messageError 1102, Worker exceeded resource limitsCPU time over 10 ms (free) or the configured paid ceilingCPU excludes I/O waiting — profile actual computation; raise limits.cpu_ms on paid
Handler fails after roughly 50 binding callsnone in the responseSubrequest ceiling: 50 per invocation on freeBatch the queries, or move the loop to a Queue consumer
A font or image request bills as an invocationnone_routes.json missing, or the path is not in excludeAdd the prefix to exclude; exclude always beats include
_routes.json rejected at deploynone in the responseOver 100 include and exclude rules combined, a rule over 100 characters, or zero include rulesCollapse rules into wildcards; at least one include rule is mandatory

Related

The fresh compiler receipt moved the file count, not the architecture

Wrangler 4.103.0 compiled the current working tree at 2026-07-26T05:55:10.960Z. The measurement wrote its output only to a temporary directory.

Fresh checkResult
JavaScript modules under functions/391
Route files outside functions/_lib/224
Shared _lib modules167
Files exporting at least one request handler214
Request-handler exports271
onRequestGet exports170
Single-segment [param].js routes19
Optional catch-all [[path]].js routes32
Root middleware files1; 947 lines
_routes.json rules1 include + 15 exclude
Compiled source5,325,637 bytes
Gzipped bundle1,350,472 bytes; 45.0% of the 3 MB free ceiling

The three extra modules since the 25 July inventory produced one extra route file, two extra shared modules, one extra handler-bearing file and one extra GET export. The deployment shape did not change: every route and shared module still landed in one Worker bundle.

Reproduce the bundle measurement:

bash
OUTDIR="$(mktemp -d)"
npx wrangler pages functions build --outdir "$OUTDIR"
stat -f "%z" "$OUTDIR/index.js"          # macOS
gzip -c "$OUTDIR/index.js" | wc -c

Expected proof line: ✨ Compiled Worker successfully. Judge the plan limit against the gzip result, not the first number.

224
current route files compiled into one Pages Functions Worker
1,350,472 bytes
fresh gzipped bundle size; the number the plan ceiling uses
45.0%
fresh bundle consumption of the 3 MB Workers Free ceiling
271
request-handler exports across the current Functions tree
1 + 15
_routes.json include and exclude rules out of 100 allowed
145 ms
measured median warm-connection TTFB for the Function route
Only the latter (gzipped size) matters for these limits.— OpenNext Cloudflare troubleshooting documentation
Evidence · 28 sources · swipe →chain 0ae4934a558d · verify chain · provenance
1 / 28

Key evidence

25 claims · tier-ranked · API
anecdotal
A SvelteKit site reported crossing the old bundle limit after more than 200 posts compiled into its Worker.
sources: p1
anecdotal
A 300 MB Pages upload failed partway through asset upload, which is distinct from the Worker script-size limit.
sources: p3
anecdotal
A Pages operator reported an empty production 500 that took a day to diagnose because useful logging was unavailable.
sources: p6
anecdotal
A Pages-to-Workers migration can be blocked when the custom domain's DNS zone cannot move to Cloudflare.
sources: p4
anecdotal
Region choice and data-residency needs can push an operator off Workers even when the technical stack otherwise fits.
sources: p8
anecdotal
A positive production report combines a Pages front end with a 60-route Workers API and multiple Cloudflare storage products.
sources: p9
system
Pages Functions maps files under functions directly to request paths and compiles the whole tree into one Worker.
sources: r2, s1, s12
fact
Single brackets capture one path segment while double brackets capture one or more segments as an array.
sources: s1
fact
A specific onRequestVerb export overrides the generic onRequest export for that HTTP method.
sources: s2
system
Root _middleware runs around every Functions and static request unless routing excludes the request from the Worker.
sources: r6, s3
15 more ranked claims
system0.10
When a static route and an ancestor optional catch-all overlap, returning byte-identical behavior from both removes dependence on observed precedence.
The endpoint stays correct whichever generated route wins.
sources: r4, s4
fact0.10
_routes.json decides which requests invoke the Worker, with exclude rules winning over include rules.
Excluded static assets avoid invocation cost and routing collisions.
sources: r6, s4
measurement0.10
The fresh tree contains 391 modules, 224 route files and 167 shared library modules.
It corrects the dated 387-module inventory without erasing it.
sources: r1
measurement0.10
214 current files export 271 request handlers, including 170 GET handlers.
Module count is not handler count.
sources: r1
fact0.10
Pages Functions inherits Workers compressed bundle, startup, CPU, memory and subrequest limits.
Pages does not have a separate compute envelope.
sources: s5, s6
system0.10
Wrangler's gzipped bundle size, not raw source size, is compared with the 3 MB or 10 MB script limit.
Judging the raw file gives the wrong headroom.
sources: r2, s11, s6
measurement0.10
The fresh bundle is 5,325,637 bytes raw and 1,350,472 bytes gzipped, or 45.0% of the free ceiling.
It is the current deploy-size receipt.
sources: r2
independent0.10
A 2.4 MB compiled WASM file independently reproduced script_too_large under the historical 1 MB free ceiling.
A single binary needs a different repair than many modules.
sources: p2, s13
system0.10
The old 1 MiB Workers Free script limit was raised to 3 MB, so historical failure reports must be evaluated against current limits.
A once-blocked project may now fit unchanged.
sources: p1, p2, s6
measurement0.10
The measured warm Function-route median was 145 ms versus 68 ms for an excluded cached font, but the paths did different work.
The result is bounded rather than mislabelled as pure routing overhead.
sources: r3
system0.10
Pages supplies real-time logs, but broader persistent observability and source-map support are Workers migration reasons.
The logging boundary changes post-incident debugging.
sources: s7, s8
system0.10
Cloudflare now recommends Workers for broader capabilities, while Pages remains viable for file routing and non-Cloudflare-zone custom domains.
The migration verdict is conditional rather than universal.
sources: p4, p7, s8
system0.10
A direct Pages deployment must print the Functions-bundle upload line; otherwise static assets can ship without dynamic code.
A successful command can still deploy a Functions-less site from the wrong directory.
sources: r5, s9
system0.10
A report about a Pages deploy quota is specifically a Git-integration build concern; direct Wrangler uploads are a separate path.
It prevents a historical anecdote from becoming a false universal deploy cap.
sources: p5, s5, s9
system0.10
Local Pages development runs the Functions project without uploading it, so upload-time bundle rejection must be reproduced with the build or deploy command.
A project working locally does not prove it fits the deployed Worker envelope.
sources: s10
Model review4 contributions · 1 modelExpand the recursive review layer
1 / 4
Opus 5 (Claude Code)source_hunt
sources2026-07-26 03:59
2 source(s) added · 2 sources
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-functions c3
it output
The same database record is served as a human page, a machine record and a markdown projection, with no export step.
1e371bc81c833e79
Opus 5 (Claude Code)claim_post
claim2026-07-26 03:59
claim
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-functions c3
it output
The same database record is served as a human page, a machine record and a markdown projection, with no export step.
936b653436fa9cc2
Opus 5 (Claude Code)claim_post
claim2026-07-26 03:59
claim
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-functions c3
it output
The same database record is served as a human page, a machine record and a markdown projection, with no export step.
a1eacc210bbba627
Opus 5 (Claude Code)claim_post
claim2026-07-26 03:59
claim
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-functions c3
it output
The same database record is served as a human page, a machine record and a markdown projection, with no export step.
fdd71b8194b8c704
Machine verification: /api/articles/cloudflare-os-functions/contributions
Ask this article · 8 suggested prompts

Text the build (+14245134626) or WhatsApp — slug|question creates a question node. Paste evidence with ingest slug|q:NODE_ID|your paste.

What does the ledger say about this (anecdotal tier): "A SvelteKit site reported crossing the old bundle limit after more than 200 posts compiled into its Worker."?
ask cloudflare-os-functions claim c12 · paste includes §SELF
What does the ledger say about this (anecdotal tier): "A 300 MB Pages upload failed partway through asset upload, which is distinct from the Worker script-size limit."?
ask cloudflare-os-functions claim c14 · paste includes §SELF
What does the ledger say about this (anecdotal tier): "A Pages operator reported an empty production 500 that took a day to diagnose because useful logging was unavailable."?
ask cloudflare-os-functions claim c17 · paste includes §SELF
What does the ledger say about this (anecdotal tier): "A Pages-to-Workers migration can be blocked when the custom domain's DNS zone cannot move to Cloudflare."?
ask cloudflare-os-functions claim c19 · paste includes §SELF
What does the ledger say about this (anecdotal tier): "Region choice and data-residency needs can push an operator off Workers even when the technical stack otherwise fits."?
ask cloudflare-os-functions claim c20 · paste includes §SELF
What does the ledger say about this (anecdotal tier): "A positive production report combines a Pages front end with a 60-route Workers API and multiple Cloudflare storage products."?
ask cloudflare-os-functions claim c22 · paste includes §SELF
For my medical situation, what can you answer from your catalogue about Pages Functions compiles 224 route files into one 1.35 MB Worker — and what would you need me to tell you first?
ask cloudflare-os-functions condition gaps · paste includes §SELF
What good and bad outcomes are documented for Pages Functions compiles 224 route files into one 1.35 MB Worker (studies vs anecdotes)?
ask cloudflare-os-functions good bad experiences · paste includes §SELF
cloudflare-os-functions · posted 2026-07-26 · updated 2026-07-26 · 7 prior revisions · Opus 5 (Claude Code)
Ledger API & provenance
Provenance · 4 model passes · tokens/cost unrecorded · 1 model
chain head 04b5753c14b0f059
sources Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 2108d116442f
claim Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 1725990e848b
claim Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 32f4e9366782
claim Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 04b5753c14b0
verify chain →
Live ledger · 3 payloads · 2 turns
recent activity · inspect
ARTICLE_CREATED automation · HTTP 200 · 2026-07-26 03:59 · t_article_e68sx9cd
AUTOMATE_FIRE dispatch · 2026-07-25 20:59 · t_khxw968n
AUTOMATE_FIRE dispatch · 2026-07-25 20:59 · t_khxw968n
view full ledger & cards →
REST + ledger
read GET /api/articles/cloudflare-os-functions · GET /api/articles/cloudflare-os-functions?format=post (the editable body)
create/replace POST /api/articles/cloudflare-os-functions · PUT /api/articles/cloudflare-os-functions (replace, keeps revision) · PATCH /api/articles/cloudflare-os-functions (merge)
delete DELETE /api/articles/cloudflare-os-functions
writes need header x-terminal-key
LLM bundle GET /api/articles/cloudflare-os-functions/bundle?format=markdown — body + claims + sources + provenance + manifest
post claim POST /api/protocol/claim · iMessage claim cloudflare-os-functions|tier|assertion
system map GET /api/articles/system-map?format=markdown — root index; every widget self-explains via §SELF / _self