The Cloudflare Workers Limits That Decide Your Build
Cloudflare Workers halts deploys for three reasons beyond code: 100,000 asset files per version, a Node heap sized by the wrong machine, and nodejs_compat.
Cloudflare Workers will refuse your deploy for three reasons that have nothing to do with your code being wrong: your version holds more than 100,000 static asset files, your build outgrows the heap Node actually gave it, or your request path quietly depends on Node.js APIs that import cleanly and throw the first time a real user hits the route. We have run into the first two on sites we maintain — a file cap ended one hosting era, and an unsized heap stopped a build mid-render — so each ceiling below now comes with a check we run before a deploy instead of after one.
That is the argument of this piece. A limit you learn about from a failed deploy is a limit you paid for twice: once in the stranded push, again in the scramble. All three ceilings are visible in advance if you measure the right thing, and none of them requires the platform to warn you first.
What "what changed" coverage leaves out
We have covered this cluster before: the decision framework for the Pages-to-Workers move, Node.js compatibility turning on for every Worker, Astro vs Next.js with real benchmarks, and yesterday's cost ledger for the move, measured. All of that answers what changed and what it costs. None answers the question a technical owner settles before the next content push: what will this platform refuse to do, and when will I find out? Coverage of limits is everywhere; coverage of your limits — the ones your growth rate walks into — is almost nowhere, because it needs someone to describe the guard instead of paraphrasing a docs page.
The three ceilings, and what each one forbids
Each row is a decision you either make now or make during an outage. The middle columns are the part most teams skip.
| Ceiling | What it forbids | What walks you into it | The guard to build | What breaks first |
|---|---|---|---|---|
| 100,000 static asset files per Worker version (20,000 on Free) | Publishing a version with more files, no matter how small each one is | Programmatic pages: every new service multiplied by every city and every language, so the count compounds with each release | A build step that counts the output and fails the deploy well below the cap | The deploy itself — and then every later push, until someone deletes pages |
| Build memory — Cloudflare publishes 8 GB for the container, nothing for your Node heap | A build that outgrows the heap Node actually gave it | Node derives its default heap from whatever machine it finds — often around 2 GB — not from the container's published 8 GB | An explicit heap ceiling in the build script, sized against the container rather than the laptop | The build dies mid-render with no artifact; production keeps serving the old version |
nodejs_compat on by default (compatibility dates ≥ 2026-08-04) | The assumption that your Worker uses no Node APIs at all | Any npm dependency that imports node:fs, node:path or node:crypto on the request path | A compatibility date you chose and reviewed, plus a dependency audit before adding one | A polyfilled module imports fine, then throws when a method is actually called |
Ceiling 1: the file cap is a content-strategy limit wearing an infrastructure costume
Cloudflare's limits page (updated October 12, 2026) lists 20,000 static asset files per Worker version on Free, 100,000 on Paid, plus a 25 MiB ceiling per file. Read it as an infrastructure row and you file it away. Read it as an architecture constraint and it changes what you build, because the cap counts files, not bytes: a 40 KB page and a 4 MB video weigh the same.
For a programmatic site that is the whole game. When pages are generated by city × service × language, the count compounds: every new service is instantly generated for every city. That means the cap arrives on a schedule you set yourself — it is a function of your content cadence, not of your traffic. Which also means you can model it before it hurts: count the built output on every release, record the delta per release, and divide the remaining headroom by that delta. If the answer is inside your planning horizon, the decision is a content-model one — hub pages, consolidation, serving the long tail from the Worker — and it is far cheaper made while it is still a line in a document.
The guard itself is small. Walk the build output, count every file except the routing artifacts, and let the number decide whether the deploy proceeds:
const LIMIT = 100_000; // the platform's per-version cap const WARN_AT = 0.8 * LIMIT; // start the content-model conversation const FAIL_AT = 0.99 * LIMIT; // stop the deploy before it happens
Below the warning line it just prints the count. Between the two it warns. At the top it exits non-zero — and if the deploy pipeline runs that check as part of its build command, a deploy that would cross the cap cannot happen by accident. That last detail is the difference between a check you remember to run and a guard that runs itself.
The asymmetry is what makes this architectural rather than operational: a failed build does not take the site down. The live deployment keeps serving the previous version while every subsequent push fails until someone frees files. Visitors notice nothing; your pipeline is dead. That is a worse failure than an outage, because an outage gets priority.
Ceiling 2: the memory number that killed our builds appears on no page
Here is the row that surprises people, because three memory numbers are in play and the one that kills builds was on none of them. Cloudflare publishes two: the runtime limits page documents 128 MB per isolate and a 64 MiB maximum Worker size, and the Workers Builds page publishes 8 GB of memory per build (2 vCPU on Free, 4 on Paid), with a 20-minute build timeout. The third number is the heap your framework's process will actually get, and no one publishes that one — Node's V8 derives its default from the machine it finds, so the same build gets a different budget on your laptop than inside the container. You are building a site of tens of thousands of files against a ceiling that shows up in no dashboard.
Ours showed us the gap the hard way, and the sequence is worth more than the outcome. The default heap was comfortable on the machine we developed on and not enough inside the build container. The push succeeded, the build died mid-render, and production quietly kept serving the previous version — which is the failure mode that costs you hours, because nothing looks broken. The first fix was to raise the number, and that was the wrong instinct: a value carried over from another machine is a preference, not a measurement. The fix that held was to treat the container as the budget and size the ceiling to it, deliberately and once, in the build script.
Two rules fall out of this. Size the heap to the container, not to the laptop. And treat a published container figure as a floor, not as your heap budget — the platform will never warn you that your framework's own default claim is the thing about to fail.
Ceiling 3: you already chose Node.js, whether you meant to or not
Since compatibility date 2026-08-04, Cloudflare enables nodejs_compat and nodejs_compat_v2 by default. You can opt out with no_nodejs_compat and no_nodejs_compat_v2, but the point is the default: any Worker created or migrated recently is running with Node's API surface switched on.
This is the ceiling teams fail quietly, because it does not look like a limit at all. Cloudflare's own documentation describes the trap in one sentence: besides natively supported APIs, Wrangler adds "polyfill shim implementations that allow you to import the module, but calling API methods will throw errors." The import graph stays green. The build passes. Then a visitor hits a route that calls fs.readFile() on the request path and gets a runtime error nobody saw in staging. The same page lists APIs that are only partially supported — console, dns, module, os, perf_hooks, test, tls — where "it imported" is not evidence that "it works."
The position that works is a deliberate one rather than an accidental one. Know which compatibility date your Worker is actually running, decide whether the Node surface should be on, and treat every dependency that reaches for a Node module as a review event rather than a default. We wrote up what actually changes under the default, including the process.env trap with older Wrangler versions — that piece answers what breaks; this one is about the decision to make before it can.
Run the same three checks
None of this requires special access. The checks are three commands and one habit:
- Count your built files, not your source files.
find dist -type f | wc -lon every production build, logged with the date. If the trajectory reaches the warning zone inside your planning horizon, the decision is a content-model one, and it is cheaper before the cap than after. - Pin the heap explicitly and never rely on the default.
NODE_OPTIONS=--max-old-space-size=<your container's budget>, set in the build script so CI and laptops agree. If your platform does not publish the budget, raise it until builds stop dying, then walk it down until they almost do — that walk is the measurement. - Read your compatibility date and audit what your dependencies import.
grep -r "node:" src/ functions/ worker/before you add a dependency, not after a route throws. If you keep the Node default on — most teams will — know which APIs are native and which are shims that only fail at call time.
The decision to make before the deploy makes it for you
A static site on Workers bets that content volume compounds in your favor — that is the point of programmatic pages — and the same compounding walks you into the file cap. Pick the moment you cross each ceiling while it is still a planning line item, or pick it at 2 a.m. with the pipeline already stopped. The platform will keep serving your last good version either way, which is exactly why the warning has to come from your own pipeline.
If you are sizing a platform move — or already hit one of these rows — that is the work we do on web architecture. Start a project and bring your file count.
Frequently Asked Questions
Does the Workers asset cap count every page my framework generates?
Yes. Every file Wrangler uploads as a static asset counts toward the per-version cap: each prerendered HTML page, each hashed JS and CSS bundle, each image, each redirect stub. There is no separate allowance for pages your framework generated. On a programmatic site the page count is the number that moves, so count the built output, not your source tree.
Why does a build that passes locally fail in Cloudflare's build container?
Because the memory your build may actually use is not the memory the container publishes. Cloudflare publishes 8 GB for a Workers Build, but Node's default heap is not that number — V8 derives it from the machine it finds, so the same build gets different headroom on a laptop and inside the container. Cap the heap explicitly with NODE_OPTIONS and size it to the container's real budget, not to your laptop.



