The Cloudflare Workers Limits That Decide Your Build
webdevelopment October 23, 2026 · Mintec

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.

CeilingWhat it forbidsWhat walks you into itThe guard to buildWhat 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 isProgrammatic pages: every new service multiplied by every city and every language, so the count compounds with each releaseA build step that counts the output and fails the deploy well below the capThe deploy itself — and then every later push, until someone deletes pages
Build memory — Cloudflare publishes 8 GB for the container, nothing for your Node heapA build that outgrows the heap Node actually gave itNode derives its default heap from whatever machine it finds — often around 2 GB — not from the container's published 8 GBAn explicit heap ceiling in the build script, sized against the container rather than the laptopThe 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 allAny npm dependency that imports node:fs, node:path or node:crypto on the request pathA compatibility date you chose and reviewed, plus a dependency audit before adding oneA 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:

  1. Count your built files, not your source files. find dist -type f | wc -l on 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.
  2. 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.
  3. 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.

Related Articles