Deploy Next.js 16 to Cloudflare Workers with OpenNext: The 2026 Migration Playbook
Ship Next.js 16 on Cloudflare Workers with the @opennextjs/cloudflare adapter. Bindings, KV cache, images, Postgres pooling, and honest cost math vs Vercel.
Deploying Next.js 16 to Cloudflare Workers takes about a day for a mid-sized app and uses the @opennextjs/cloudflare adapter, which compiles the Next.js server output into a single Worker with full Node.js compatibility, ISR through KV, and static assets served from Workers Assets. In my migrations off Vercel this year, teams landed on Cloudflare Workers for two reasons: predictable egress pricing and the same edge network as their existing R2/D1 stack. This guide walks through the whole move — codemods, adapter config, bindings, revalidation, and the traps the docs undersell.
Use @opennextjs/cloudflare (OpenNext v3+), not the deprecated @cloudflare/next-on-pages adapter. Pages is being folded into Workers Assets in 2026.
Cloudflare Workers now support the full Node.js runtime via nodejs_compat, so most Next.js server components, Server Actions, and Route Handlers run unmodified.
ISR and revalidateTag require binding a KV namespace as the incremental cache; skipping this step is the #1 reason on-demand revalidation silently fails.
Total migration effort for a mid-sized SaaS app: ~1–3 days including image optimization, Postgres connection pooling changes, and cron rewrites.
Cold starts on Workers are ~5–15 ms versus 200–800 ms on Vercel Node functions, but you lose Vercel's Fluid Compute pricing model and some Next.js features (draft mode preview URLs need manual setup).
Cloudflare bills CPU time (not wall-clock), so long-running database queries stay cheap; sustained heavy compute apps are usually 30–60% cheaper than the equivalent Vercel Pro plan.
Can Next.js 16 run on Cloudflare Workers?
Yes, Next.js 16 runs on Cloudflare Workers today through the OpenNext Cloudflare adapter, which is the officially recommended path since Cloudflare joined the OpenNext project in mid-2025. The adapter transforms the standard .next build output into a single Worker bundle, resolves Node.js APIs against the nodejs_compat compatibility flag, and hands static assets to Cloudflare's Workers Assets binding. React Server Components render at the edge, Server Actions execute against your bound databases and queues, and Route Handlers stream responses with the same signatures you already use on Node.
The compatibility surface is wider than most Vercel-native teams expect. In practice, the pieces that migrate cleanly include the full App Router, fetch caching, middleware (now proxy.ts in Next.js 16), streaming with Suspense, Server Actions with FormData, and the Node crypto/streams/buffer APIs that Auth.js v5 and Drizzle depend on. What does not port automatically: Vercel's Image Optimization backend (you swap in the Cloudflare Images loader), after() background execution (Workers use waitUntil instead), and Draft Mode preview cookies for teams that relied on the Vercel-hosted preview URL flow.
OpenNext vs @cloudflare/next-on-pages: which adapter to pick
Two adapters historically shipped Next.js to Cloudflare, and their trade-offs matter enough that I want to lay them side-by-side before touching code. Framework-neutral summary: OpenNext is the strategic path forward, and Cloudflare's own docs now point at it.
Feature
@opennextjs/cloudflare (Workers)
@cloudflare/next-on-pages (Pages, deprecated)
Vercel (reference)
Runtime
Full Node.js (nodejs_compat v2)
Edge-only (Workers V8)
Node.js + Edge functions
App Router support
Complete
Partial (no Node-only APIs)
Complete
Server Actions
Yes
Yes, but Node crypto limited
Yes
ISR / on-demand revalidation
KV-backed, fully supported
Manual, buggy
Built-in
Image Optimization
Cloudflare Images loader or Polish
Same
Built-in (metered)
Cold start
5–15 ms
3–8 ms
200–800 ms (Node), 50–120 ms (Edge)
Free tier requests/day
100,000
100,000
Unlimited (hobby, with limits)
Recommended for new projects
Yes
No (deprecated)
If you need Fluid Compute pricing
The pricing distinction I flag for every team: Cloudflare charges by CPU-milliseconds, not wall-clock. If your app spends 3 seconds waiting on a slow Postgres query, Cloudflare bills you for the ~5 ms of CPU work, whereas Vercel's Function GB-second model bills roughly the entire 3 seconds. On workloads dominated by I/O (which describes most SaaS apps), the CPU-time model is dramatically cheaper. On workloads dominated by compute (image processing, LLM inference in-process), Vercel Fluid Compute closes the gap. Match the pricing model to your app before committing.
The migration playbook: Vercel to Cloudflare in 3 days
I estimate migrations off Vercel to Cloudflare Workers at 1–3 engineering days for a Next.js 16 App Router app with typical primitives: Postgres, an auth library, Stripe, Resend, and a background job or two. The plan below is what I've run on four teams this year, and it's nothing fancy, just an honest ordering that surfaces bindings issues before you ship.
Day 1: adapter install, local build, smoke test
Install the adapter and Wrangler: pnpm add -D @opennextjs/cloudflare wrangler. Verify Wrangler version 4.20+ for Workers Assets support.
Create open-next.config.ts at the project root with the Cloudflare adapter preset.
Add wrangler.jsonc with compatibility_date set to a 2026 date and the nodejs_compat flag.
Run opennextjs-cloudflare build, then opennextjs-cloudflare preview and click through the app locally. Fix any node module resolution errors before touching production.
Day 2: bindings, environment, and data
Bind a KV namespace for the incremental cache (required for ISR; see the next section).
Migrate connection pooling. Native pg works on Workers with nodejs_compat, but for Postgres I move teams to Neon's HTTP driver or a shared pooler like PgBouncer, because each Worker isolate opens its own TCP socket and you'll exhaust connection limits fast otherwise.
Migrate environment variables via wrangler secret put. Do not commit them to wrangler.jsonc. Public NEXT_PUBLIC_* vars go under vars.
If you rely on after() for post-response work (sending analytics, revalidating tags), swap to Cloudflare's ctx.waitUntil. OpenNext exposes a wrapper so you don't have to plumb the execution context yourself.
Day 3: images, cron, staging, cutover
Configure next/image to use Cloudflare Images or the Workers Assets loader. Guidance in the images section below.
Deploy to a staging Worker: opennextjs-cloudflare deploy --env staging. Run your Playwright suite against it.
Switch DNS (Cloudflare-managed, so a one-click custom domain attach), monitor error rates in Workers Logs, and keep the Vercel deployment warm for 24 hours as a rollback target.
Configure the @opennextjs/cloudflare adapter
Two config files do 95% of the work. The first tells OpenNext which adapter to use; the second tells Wrangler how to run the resulting Worker. This is the minimal set that will build a Next.js 16 app locally against the Miniflare-based preview and deploy to production unchanged.
// open-next.config.ts
// Runs at build time; produces .open-next/worker.js from the Next.js output.
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache";
export default defineCloudflareConfig({
// KV binding used for the ISR incremental cache. Must match the
// binding name in wrangler.jsonc below.
incrementalCache: kvIncrementalCache,
});
// wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "acme-web",
"main": ".open-next/worker.js",
"compatibility_date": "2026-09-01",
// nodejs_compat gives you the full Node.js runtime (crypto, buffer, streams).
// Required for Next.js 16 server components and most auth libraries.
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"kv_namespaces": [
// Binding name must match the incrementalCache reference above.
{ "binding": "NEXT_INC_CACHE_KV", "id": "<kv-namespace-id>" }
],
"observability": { "enabled": true }
}
Add three scripts to package.json so the day-to-day loop stays fast:
Under the hood, opennextjs-cloudflare build runs next build, then walks the output tracing files, rewrites Node module imports for the Workers runtime, and emits a single worker.js entrypoint alongside a static assets/ directory. If a dependency uses a truly Workers-incompatible API (native modules, child_process), the build fails loud with a file path. Do not paper over these with polyfills; find a Workers-compatible alternative or run the affected route on a different platform.
Wire up ISR and revalidation with KV
The most common footgun in this migration is that revalidateTag and revalidatePath silently no-op if you skip the KV incremental cache binding. Symptoms: Server Actions succeed, the page revalidates locally, but production keeps serving the stale HTML. The fix is a two-minute binding + one-line config change.
# Create the KV namespace once per environment.
npx wrangler kv namespace create "NEXT_INC_CACHE_KV"
# Copy the returned id into wrangler.jsonc under kv_namespaces.
# For local preview, create a preview namespace too:
npx wrangler kv namespace create "NEXT_INC_CACHE_KV" --preview
With the binding in place, on-demand revalidation just works. Here is a Server Action that updates a product and revalidates the tag, identical to what you'd write on Vercel:
// app/products/actions.ts
"use server";
import { revalidateTag } from "next/cache";
import { db } from "@/lib/db";
export async function updateProductStock(productId: string, stock: number) {
await db.update("products").set({ stock }).where({ id: productId });
// OpenNext's KV cache adapter picks this up and evicts the tagged entry
// across all Cloudflare edge locations within ~seconds.
revalidateTag(`product:${productId}`);
}
For stronger consistency, swap the KV cache for the R2 incremental cache adapter. It uses R2's strong consistency at the cost of ~50 ms extra origin latency on cold cache reads. Most teams don't need it. If you do, the swap is a one-line change in open-next.config.ts from kv-cache to r2-cache.
Images, R2 assets, and Postgres connection pooling
Three integration points bite hardest during migrations. I'll cover each with the working code.
next/image on Cloudflare
Cloudflare doesn't run Vercel's image optimizer, so you point next/image at Cloudflare Images (paid) or Workers Assets (free, no transformations) via a custom loader. The custom loader approach works well when you're serving pre-optimized images out of R2:
Each Cloudflare Worker isolate is short-lived and can spawn a new TCP connection per request. Under any real traffic, a naive pg setup exhausts Postgres max_connections in minutes. Two workable patterns: the Neon serverless HTTP driver (my default for greenfield), or a Supabase/PgBouncer transaction-pooled connection URL. Skip Prisma Data Proxy; it's an extra network hop for problems you can solve at the driver layer.
// lib/db.ts (Neon HTTP driver, one connection-less request per query)
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
If you're on stock Postgres without Neon, front it with PgBouncer in transaction mode and set the pool size to something like 20 connections. Every Worker request will grab a pooled connection, run the query, and release. This is the same pattern you'd use if you were self-hosting Next.js with Docker behind a load balancer. Nothing Cloudflare-specific about it.
R2 for large static assets
Workers Assets caps individual files at 25 MB. For anything bigger (video, ML model weights, downloadable PDFs), bind an R2 bucket and stream from it. R2 has zero egress fees, which is the single biggest cost delta versus S3 or Vercel's blob storage for high-bandwidth apps.
Cost and performance: Cloudflare vs Vercel vs self-host
The honest comparison isn't "Cloudflare cheaper than Vercel," it's "which pricing model matches your traffic shape." I've dropped my back-of-envelope numbers below for a Next.js 16 SaaS app doing 10M page views/month with typical mix (30% dynamic, 70% ISR cached).
Dimension
Cloudflare Workers
Vercel Pro
Self-host (Docker on Hetzner)
Monthly cost @ 10M views
~$45
~$180
~$60 + ops time
Cold start p95
15 ms
400 ms (Node) / 90 ms (Fluid)
0 ms (long-lived)
Global regions
320+ PoPs
~20 (Node), 300 (Edge)
1 (add CDN yourself)
ISR support
KV or R2
Built-in
Filesystem or Redis
Feature parity with Next.js
~95%
100%
~90% (no image API)
Time to first deploy
~30 min
~5 min
~2 hours
Operational overhead
Low
Zero
Medium
The pattern I see: teams under 1M monthly views stay on Vercel Hobby/Pro because the DX premium is worth $180/month. Teams between 1M and 100M views often save meaningfully on Cloudflare Workers, especially if they already use R2 or D1. Teams over 100M views usually end up on a hybrid: Cloudflare Workers for the public marketing pages, self-host for the authenticated app tier where you want long-lived process caches. Framework choice does not lock you into an infrastructure choice, and that's the whole point of OpenNext.
Common pitfalls and how I debug them
Five issues surface in almost every migration. Fixing each is fast once you know where to look.
1. "process is not defined" during build
You forgot the nodejs_compat flag in wrangler.jsonc, or your compatibility_date predates its default-on status (mid-2024). Add the flag explicitly and rebuild.
2. revalidateTag silently does nothing in production
Missing KV binding, or a mismatched binding name between open-next.config.ts and wrangler.jsonc. Verify with wrangler kv key list --binding NEXT_INC_CACHE_KV and confirm the namespace has entries after your first cached page load.
3. Server Actions return 500 with no stack trace
Enable Workers observability ("observability": { "enabled": true }) and open the Workers Logs tab in the Cloudflare dashboard. Nine times out of ten it's a Postgres connection exhaustion issue. Check your driver's connection count metric. This is the same class of bug as the one I documented in the article on Next.js cache not revalidating after Server Actions, just with a different root cause.
4. Cold-start higher than expected
Your Worker bundle probably ballooned past 3 MB gzipped. Run opennextjs-cloudflare build --analyze to find the culprit. Common offenders: shipping unminified Firebase SDKs, importing full lodash instead of specific functions, or including Node polyfills you don't actually need now that nodejs_compat handles them.
5. next/image throws "unoptimized" in production
Your custom loader is missing or returning malformed URLs. Add console.log inside the loader function during preview and confirm the URL resolves to a real Cloudflare Images (or R2) endpoint. Check that the domain you're pulling from is enabled for Cloudflare image transformations. It's a per-zone toggle in the Speed section of the dashboard.
When to reach for Cloudflare (and when not to)
Reach for Cloudflare Workers with OpenNext when your app is I/O-heavy (SaaS dashboards, marketplaces, content sites), you already use R2/D1/Images in your stack, or your Vercel bill has crossed the point where a day of engineering time pays back inside a quarter. Stay on Vercel when your team values zero-config DX above all, uses Fluid Compute-friendly workloads like in-process AI inference, or you rely heavily on Vercel-exclusive features (Speed Insights, Enterprise Edge Config, Draft Mode preview URLs). Consider self-hosting when you have 24/7 traffic that would let you amortize a long-lived VM, or your compliance posture requires infrastructure you fully own. The trade-off I broke down in the Edge Runtime vs Node Runtime guide applies here too. Cloudflare isn't automatically better; it's just the newest credible option in a market that used to have exactly one.
Frequently Asked Questions
Is OpenNext for Cloudflare production-ready in 2026?
Yes. OpenNext v3+ with the @opennextjs/cloudflare adapter is the recommended production path for Next.js 16 on Cloudflare Workers, endorsed by both Cloudflare and the OpenNext project. Several public production apps run on it, including community projects with 8-figure monthly page views.
What's the difference between Cloudflare Workers and Cloudflare Pages for Next.js?
Workers is Cloudflare's compute runtime; Pages is the legacy static-plus-functions product that used the deprecated @cloudflare/next-on-pages adapter. As of September 2026, new Next.js projects should target Workers directly via OpenNext (Pages' Next.js support is being folded into Workers Assets).
Does OpenNext support Next.js Server Actions?
Yes, fully. Server Actions with FormData, useActionState, and useOptimistic all work on Cloudflare Workers via OpenNext, provided you've enabled nodejs_compat and bound a KV namespace for the incremental cache used by revalidateTag.
How much does it cost to deploy Next.js 16 to Cloudflare Workers?
The free tier covers 100,000 requests per day and 10 ms of CPU per request — enough for hobby projects and small SaaS apps. Paid plans start at $5/month for 10 million requests. In practice, a 10M-view SaaS app costs around $45/month on Cloudflare versus ~$180 on Vercel Pro for a comparable workload.
Can I keep using Vercel Analytics and Sentry after migrating?
Sentry works unchanged — server-side capture continues to run through the standard Node SDK. Vercel Analytics won't function outside Vercel; swap to a hosting-agnostic option like Cloudflare Web Analytics, PostHog, or Plausible before cutover.
The Next.js preload pattern uses React cache() and a void preload() helper to eliminate server component fetch waterfalls. Learn the DAL setup, Suspense streaming, and ORM memoization tricks that shave 300-900ms off real routes.
Server-side feature flags in Next.js 16 without client bundles or flash-of-wrong-variant. Walk through install, Edge Config storage, precomputation for static routes, percentage rollouts, kill switches, and how the SDK compares to LaunchDarkly, Statsig, and GrowthBook.
Learn how to install Better Auth in Next.js 16, wire up Drizzle or Prisma, add email/password plus Google and GitHub social login, and lock down routes with proxy.ts and Server Action session checks.