| name | cloudflare-pages |
| description | Use when deploying a Next.js app to Cloudflare Pages, migrating from Vercel to Cloudflare, troubleshooting edge runtime errors, or evaluating whether a Next.js project is viable on Cloudflare's free or paid plan. |
| when_to_use | User mentions Cloudflare Pages, @cloudflare/next-on-pages, @opennextjs/cloudflare, wrangler, worker size limits, or asks about moving a Next.js app off Vercel to Cloudflare. |
| allowed-tools | Bash, Read, Edit, Write |
Migrating Next.js from Vercel to Cloudflare Pages
Conclusion
Not viable for large Next.js apps on Cloudflare's free plan. The bundled worker size is the hard blocker. Lightweight projects (few dependencies) work fine.
Two Cloudflare Adapters — Pick the Right One
@cloudflare/next-on-pages (older)
- Splits each route into a separate edge function
- Requires every dynamic route to export
runtime='edge'
- Hard incompatibility: Next.js throws if you combine
runtime='edge' with generateStaticParams in the same route
- Any Node.js module (
fs, path, child_process) anywhere in a route's import chain breaks the build
- Not recommended for Nextra or content-heavy apps
@opennextjs/cloudflare (recommended)
- Bundles the entire app into a single
handler.mjs worker
- Does NOT require
runtime='edge' on all routes
- Supports Node.js APIs via the
nodejs_compat Cloudflare compatibility flag
- Correct choice for apps with Node.js-dependent server components
Migration Steps (@opennextjs/cloudflare)
- Install:
pnpm add -D @opennextjs/cloudflare wrangler
- Create
open-next.config.ts:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
- Create
wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "<your-cloudflare-worker-name>",
"compatibility_date": "<today>",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"observability": { "enabled": true }
}
- Worker name must match the name in your Cloudflare dashboard exactly
- Only add
services / WORKER_SELF_REFERENCE binding if you use ISR (export const revalidate = X at page level) — most apps don't need it
- In Cloudflare Pages dashboard: set build command to
opennextjs-cloudflare build, output directory to .open-next/assets
The Real Blocker: Worker Size
@opennextjs/cloudflare collapses everything into one handler.mjs. Cloudflare enforces a compressed size limit:
| Plan | Limit |
|---|
| Free | 3 MiB |
| Paid | 10 MiB |
Always measure before deploying:
opennextjs-cloudflare build
du -sh .open-next/server-functions/default/handler.mjs
Real-world examples:
- Lightweight Nextra docs site (minimal deps): ~4 MB — works on paid, too large for free
- Full-featured Next.js app (Sandpack, GSAP, Radix UI, component registry): ~42 MB — exceeds even the paid plan
Important: Shiki language chunks and .next/server/chunks/ are served as CDN assets and do NOT count toward the worker size limit. The worker size is driven by node_modules bundled into the worker context — primarily heavy packages imported by server components or API routes.
If the Worker Is Too Large
Options in order of effort:
- Upgrade plan — only viable if
handler.mjs is between 3-10 MB
- Remove or lazy-load heavy dependencies — likely candidates: Sandpack, GSAP, icon libraries, OG image generation (
@vercel/og adds ~3.6 MB via resvg.wasm)
- Limit Shiki language packs — configure Nextra/Shiki to only bundle languages actually used
- Accept it's not viable — some apps are simply too large for Cloudflare's worker model
Pitfalls to Avoid
runtime='edge' on API routes for Cloudflare breaks Vercel — Vercel's edge bundler may flag Node.js module references that are fine in Node.js runtime. Only add runtime='edge' to routes that genuinely need it (stateless, no Node.js APIs). Revert these if moving back to Vercel.
WORKER_SELF_REFERENCE service binding — only needed for ISR. The binding name must match your exact worker name or the deploy fails with code 10143.
- Worker name mismatch — the
name in wrangler.jsonc must match the Cloudflare dashboard worker name exactly (visible in CI warning if wrong).
- Cloudflare doesn't auto-update build settings — when switching adapters on an existing project, manually update the build command and output directory in the dashboard.