The OpenNext error you'll hit deploying Next.js to Cloudflare

A walkthrough of the bundle-too-large, dynamic-import, and `nodejs_compat` flag combinations that bite every Cloudflare Next.js team at least once.

·
cloudflarenextjs

You've spent weeks building a Next.js app that runs perfectly on your laptop and on Vercel. You decide to move to Cloudflare for the performance and the pricing. You run npx open-next@latest build and wrangler deploy. The CLI spins for a while, then dies with a cryptic line: Script exceeded maximum allowed size. Or worse — the deploy succeeds, and your production site is a wall of 500s because some nested dependency tried to call fs.readFileSync on an edge runtime that has no filesystem. Welcome to the impedance mismatch between the Node-heavy Next.js ecosystem and the lean V8 isolate world of Cloudflare.

Why OpenNext Is the Bridge (and Where It Cracks)

OpenNext isn't a thin wrapper around next build. It's a transformation engine that takes Next.js's Node-centric server output and reshapes it to run on Cloudflare's edge runtime. It bundles your server code, middleware, and route handlers into a Worker, maps Next.js's caching and ISR onto Cloudflare primitives, and stitches the whole thing together so the framework thinks it's running on Node.

The fundamental difference between V8 isolate and Node.js

A Node.js server boots a full runtime: a filesystem, native modules, a persistent process, the entire standard library. A Cloudflare Worker runs in a V8 isolate that starts in milliseconds precisely because it has none of that. No fs, no net, no persistent process, strict memory and CPU caps, and a hard limit on how big your script can be. Next.js was designed for the former. OpenNext's job is to translate it to the latter, and most deployment failures happen at the seams where that translation is imperfect.

How OpenNext structures the build

After a build, you get a .open-next/ directory containing the Worker entrypoint, your server bundle, static assets destined for Cloudflare's asset hosting, and the middleware compiled separately. Understanding that the server code becomes a single Worker script is the key to understanding the number-one failure mode: that script has a size limit.

The 10MB Wall: "Script exceeded maximum allowed size"

This is the most frequent failure, full stop. Cloudflare caps the compressed size of a Worker script — 1MB on the free plan, 10MB on paid. A non-trivial Next.js app blows past these limits with alarming ease.

Why Next.js bundles grow so fast

The culprit is almost always node_modules. A date library that pulls in every locale, an icon package where you import one icon and bundle ten thousand, an SDK that ships its entire surface area — these inflate the server bundle fast. On Vercel nobody notices because there's no script-size limit. On Cloudflare it's a hard wall.

Tree-shaking failures in the OpenNext pipeline

Tree-shaking only works when imports are statically analyzable. A library that uses dynamic require() or a barrel index.js re-exporting everything defeats it, and the whole package ends up in your bundle. The fix is to find the heavy dependencies and either import more narrowly or swap them out:

# wrangler.jsonc — make sure minification is on
{
  "name": "my-next-app",
  "main": ".open-next/worker.js",
  "minify": true,
  "compatibility_flags": ["nodejs_compat"]
}

Then run a bundle analyzer against the OpenNext output to see what's actually taking up space. The usual offenders: moment (swap for date-fns or native Intl), unscoped icon imports, and SDKs that aren't edge-aware. Trimming two or three of these typically gets you back under the limit.

The nodejs_compat Flag: The Silent Killer

Many Next.js libraries assume a full Node environment. Cloudflare's nodejs_compat flag exists to paper over the gap — but it papers over some of it, not all.

What nodejs_compat actually enables

With the flag set, Cloudflare provides implementations of common Node built-ins: Buffer, crypto, parts of stream, util, path, and more. This covers a large swath of libraries. What it does not give you is anything that requires actual operating-system resources — fs reading from a real disk, child_process, raw TCP sockets via net. There is no filesystem at the edge, so no compatibility shim can make fs.readFileSync('./template.html') work at runtime.

Identifying hidden Node dependencies

The dangerous ones are libraries that use these APIs deep in their internals, where you'd never see them. A JWT library that reads a key file, an image library that shells out, a database driver that opens a raw socket — these compile fine and fail at runtime. The defense is to prefer edge-ready alternatives: use jose instead of jsonwebtoken, use HTTP-based database drivers (the Neon serverless driver, Cloudflare D1, PlanetScale's fetch driver) instead of TCP ones, and audit any dependency that touches the filesystem before you ship it.

The Dynamic Import and require() Trap

OpenNext struggles most with libraries that resolve modules dynamically, and these produce the nastiest class of bug: a runtime "Module not found" that the build never caught.

Why next/dynamic behaves differently on the edge

On Node, a dynamic import or conditional require() resolves against a real module graph on disk at runtime. At the edge there is no module graph to resolve against — everything must be bundled ahead of time. If a library decides at runtime which module to load based on, say, an environment check, the bundler can't know which branch to include, and the import fails the moment that code path runs in production.

Build logs vs. runtime error logs

This is what makes it so frustrating: the build succeeds because the problematic require() is inside a code path that never executes during the build. The error only surfaces when a real request hits that path in production. Your build log is green; your runtime log is on fire. The lesson is that a successful OpenNext build is necessary but not sufficient — you have to exercise the real routes, which is exactly what observability is for.

Middleware vs. the Edge Runtime

Next.js Middleware already runs on the edge by default, but under OpenNext the environment is even more restrictive than the main server output.

The double-bundle problem

Middleware is compiled into its own bundle, separate from your server code, and it counts against your size budget too. Import a heavy library in middleware.ts — an auth SDK, a full validation library — and you can blow the limit even if your main app is lean. Keep middleware tiny: header manipulation, redirects, lightweight auth checks. Push anything heavy into a route handler.

Fetch API quirks

// Works on Vercel, can break on Cloudflare:
// - assuming Node streams instead of web streams
// - relying on header behaviors the edge normalizes differently
export function middleware(request) {
  const token = request.headers.get("authorization");
  // Heavy JWT verify here will bloat the middleware bundle.
  // Prefer a lightweight, edge-native check and verify fully downstream.
  if (!token) return Response.redirect(new URL("/login", request.url));
  return NextResponse.next();
}

Observability: Catching the Errors OpenNext Doesn't See

The hardest OpenNext failures are the ones where the deploy succeeds and the app crashes on the first real request — often before your logging is even initialized.

Capturing startup crashes

When a hidden Node dependency throws during module initialization, the crash happens during the isolate's startup handshake, before any code you wrote runs. Standard logging never gets a chance. A Sentry-compatible SDK that hooks the Worker's top-level error handling — and a Tail Worker that catches exceptions out-of-band — are how you see these. The Tail Worker is especially valuable here because it receives the exception even when the producing isolate died on the way up.

Reading edge stack traces

The traces you do capture will point at minified bundle offsets like worker.js:1:84120 unless you upload source maps. OpenNext can emit them; wire the upload into your deploy so incoming errors get deminified against the right bundle. Our source maps documentation covers the mechanics, and for the broader edge-debugging toolkit see debugging Cloudflare Workers in production.

The Pre-Deployment Checklist for OpenNext

  • Prune dependencies before the build. Run a bundle analyzer, swap fat libraries for edge-friendly ones, and import narrowly to keep tree-shaking effective.
  • Audit for hidden Node APIs. Grep your dependency tree for fs, child_process, and net usage; replace anything that reads from disk or opens raw sockets.
  • Keep middleware minimal. Remember it's a separate bundle that counts against your size limit.
  • Test with wrangler dev --remote. Local dev uses a Node-ish shim; --remote runs against the real edge runtime and surfaces the filesystem and dynamic-import failures before production does.
  • Wire up edge error tracking. Catch the startup crashes and dynamic-import failures that a green build hides.

For the Next.js-specific error patterns that show up once you're deployed, our guide on error tracking in Next.js 15 picks up where this one leaves off, and if you hit the dreaded runtime termination, why Workers throw "Script will never generate a response" covers the shared root causes.

Moving Next.js to Cloudflare is absolutely worth it for the performance and the economics — but the transition has a specific set of traps, and almost all of them come down to the same thing: code written for Node running in a place that isn't Node. Get under the size limit, kill the hidden filesystem dependencies, test against the real edge runtime, and instrument the startup path. GlitchReplay captures full session replays and Sentry-compatible, source-map-resolved stack traces for Cloudflare-deployed Next.js apps, including the startup crashes that never make it to a normal log — all at a flat rate with no per-event fees. If your OpenNext deploy keeps failing in ways the build won't explain, give GlitchReplay a try.

Stop watching your error bill spike.

GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.