Skip to content
Fangfang Chen
← Back to writing

Author: Fangfang Chen · Engineering · November 4, 2026

Shipping Next.js on Cloudflare, without Vercel

Pages to Workers, bundle accounting, and the limits that actually matter.


The first Cloudflare deployment did not fail because the code was too slow. It failed because the build was counted in the wrong unit.

VieauQc started on Cloudflare Pages with next-on-pages. The Pages Functions build hit 27,255,196 bytes — about 25.99 MiB — against a 25 MiB raw-byte limit. The same build was only 6.43 MiB gzipped. On Workers, the compressed size was the number that mattered. That difference triggered the migration from Pages to Workers with OpenNext on 16 July 2026. It was not a move away from Vercel; Vercel was never the production host.

The first OpenNext build was 5.47 MiB gzipped. By mid-September it was 9.42 MiB, or 94.2% of the old 10 MB paid cap. Then Cloudflare changed the rule: since 4 September 2026, only uncompressed size counts, with a 64 MiB limit and a one-second startup requirement. Measured startup was 27 ms. The constraint moved, but it did not disappear.

By 12 October the deploy was 50.57 MiB, 79% of the new limit. The breakdown was more useful than the total: about 12.93 MiB of repeated per-route client-reference manifests, 21.35 MiB of shared chunks, 5.77 MiB of page code, 2.25 MiB of Next runtime, and roughly 9.8 MiB of guide bodies inside the chunks.

Two lessons came out of that investigation.

First, dynamic import() does not remove code from a Worker bundle. It controls when the code runs, not whether it is shipped. The guide modules were already loaded through dynamic imports; they still counted.

Second, the durable wins came from moving data and fixing packaging, not from adding framework features. Dialogues and glossary data moved to R2 in May 2026. News bodies followed on 8 August. Grammar and reading content moved on 2 September. Guide bodies are still in the Worker bundle, which is why they remain the largest block.

The cheapest packaging fix was one line: "minify": true in wrangler.jsonc. Wrangler was re-bundling the already-compressed OpenNext output without minification, which added about 7.3 MiB. The deploy went from 50.71 MiB to 43.36 MiB on 13 October. As of 4 November the log shows 48.405 MiB, or 75.6% of the 64 MiB limit.

There are other operational rules that matter as much as the bundle:

  • Environment variables are split by build time and runtime. A stale worktree once inlined a development Clerk key into production and returned HTTP 500 on every page. The build now fails if required build-time values are missing or if a pk_test_ key appears.
  • R2 content is synced separately from code. A deploy does not push audio or article bodies. When the sync is missed, pages can render with blank bodies and no error. That happened on 13 August, 29 October and 3 November 2026.
  • Deployment is manual from main. A git push does not deploy.

None of this is specific to Next.js. It is the ordinary work of putting a content-heavy application on an edge runtime: decide what is code, what is data, what is built, and what is fetched. The deployment is boring now, which is the point.