· nextjs / cloudflare / workers
How to deploy Next.js to Cloudflare Workers (2026)
Deploy Next.js to Cloudflare Workers: @opennextjs/cloudflare adapter, wrangler.jsonc config, 330 edge cities in 90 seconds, and GitHub Actions CI/CD pipeline.
By Ethan
1,490 words · 8 min read
Cloudflare Workers is the right place to run Next.js in 2026. You get edge delivery across ~330 cities, no cold starts, a generous free tier, and the Workers ecosystem (D1, R2, KV) for everything stateful. Global deploy propagation runs in about 90 seconds — not minutes.
The adapter that makes this work is @opennextjs/cloudflare. Cloudflare co-maintains it alongside the OpenNext team. The old @cloudflare/next-on-pages adapter is superseded — if you have it installed, remove it before following this guide.
Who this is for
Next.js developers who want to ship on Cloudflare’s infrastructure. You should be comfortable with the terminal and npm/pnpm. This guide covers both new projects and migrating an existing app.
Prerequisites
- Node.js 20 or later
- npm or pnpm
- Cloudflare account (free at cloudflare.com)
- Wrangler CLI 4.x — install with
npm install --save-dev wrangler@latest
Create or adapt a Next.js app
New project: the fastest path is Cloudflare’s create command, which scaffolds a Next.js app already wired for Workers:
npm create cloudflare@latest -- my-next-app --framework=next
Existing project: skip to the next section — you’ll add the adapter manually.
If you prefer the vanilla Next.js starting point:
npx create-next-app@latest my-next-app
cd my-next-app
Use App Router and TypeScript. The adapter supports Pages Router too, but App Router is the forward-looking choice.
Install @opennextjs/cloudflare
npm install @opennextjs/cloudflare@latest
npm install --save-dev wrangler@latest
As of this writing the latest stable version is @opennextjs/[email protected] (released May 2026).
If your project previously used @cloudflare/next-on-pages, remove it completely — including any setupDevPlatform() calls in next.config.ts. The two adapters conflict at build time.
npm uninstall @cloudflare/next-on-pages
Configure wrangler.jsonc
Create wrangler.jsonc in your project root. The JSONC format supports inline comments and is preferred by the Cloudflare tooling:
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "my-app",
"compatibility_date": "2024-12-30",
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "my-app"
}
]
}
What each field does:
main: points at the Worker entry file the OpenNext adapter producescompatibility_date: must be2024-09-23or later — this date activates the full Node.js compatibility layer;2024-12-30is a safe, stable choicenodejs_compat: required flag; without it, most Next.js server code will fail at runtimeglobal_fetch_strictly_public: required for correct fetch behavior from inside the Workerassetsbinding: this is what lets Workers serve static files — previously a Pages-only feature, now available to Workers via this bindingWORKER_SELF_REFERENCEservice binding: required for the adapter’s internal routing; missing it causes 500 errors onworkers.dev
Replace "my-app" with your actual Worker name in both the name field and the service value under services.
Add supporting files
open-next.config.ts
The adapter needs this config file at the project root. If you want ISR (Incremental Static Regeneration) to work, you must also configure the R2 incremental cache — without it, ISR falls back to full SSR on every request:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
});
If you’re not using ISR, a minimal config without the cache override is fine to start:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
next.config.ts
Add the dev integration call so local next dev can connect to Wrangler bindings:
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();
const nextConfig = {
// your existing config
};
export default nextConfig;
.dev.vars
This file holds local-only environment variables that Wrangler reads during preview mode:
NEXTJS_ENV=development
Add .dev.vars to .gitignore — it should never be committed.
package.json scripts
Replace or add these to your scripts block:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy"
}
}
Also add .open-next/ to .gitignore:
.open-next/
.dev.vars
Local development
Two modes, different purposes:
npm run dev runs Next.js’s standard Node.js dev server. Fast hot-reload, great for iterating on UI. It does NOT use the Workers runtime.
npm run preview builds the app through OpenNext, then runs it locally in workerd — Cloudflare’s actual Workers runtime. Slower to start, but production-accurate. Always use preview to test ISR, middleware, caching behavior, and any Workers bindings (KV, D1, R2) before deploying.
The distinction matters: code that runs fine under next dev can fail at runtime in workerd if it depends on a Node.js API not yet available at your compatibility date.
Deploy via CLI
npm run deploy
This runs opennextjs-cloudflare build to compile your app into .open-next/, then opennextjs-cloudflare deploy which calls Wrangler under the hood. Wrangler bundles the Worker and uploads it to Cloudflare’s network. Propagation to all ~330 edge cities takes roughly 90 seconds.
The first deploy also creates the Worker in your Cloudflare account. Subsequent deploys update it in-place.
Connect GitHub for auto-deploy
Create .github/workflows/deploy.yml:
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Lint & Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
env:
NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }}
deploy:
name: Deploy
needs: verify
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Two GitHub secrets to add under Settings → Secrets and variables → Actions:
CLOUDFLARE_API_TOKEN: generate at Cloudflare Dashboard → My Profile → API Tokens → “Edit Cloudflare Workers” templateCLOUDFLARE_ACCOUNT_ID: visible in the right sidebar of the Cloudflare Dashboard when you’re on the Workers & Pages overview
For runtime secrets (database URLs, API keys), use Workers Secrets instead of GitHub env vars — they’re encrypted at rest and never appear in the build artifact:
wrangler secret put MY_SECRET
Caveats and limitations
| Feature | Status | Notes |
|---|---|---|
| App Router | ✅ | Full support |
| Pages Router | ✅ | Full support |
| Server Components | ✅ | — |
| SSG | ✅ | — |
| SSR | ✅ | — |
| ISR | ✅ | Requires R2 bucket + r2IncrementalCache override |
| Server Actions | ✅ | — |
| Middleware | ✅ | — |
| Image Optimization | ✅ | Requires Cloudflare Images binding; not free |
| Partial Prerendering (PPR) | ✅ | — |
after() | ✅ | — |
'use cache' | ✅ | — |
| Turbopack | ✅ | — |
| Node.js in Middleware (Next.js 15.2+) | ❌ | Planned for a future release |
export const runtime = "edge" | ❌ | Remove from all route files before deploying |
| Windows local dev | ❌ | Use WSL or Linux CI |
One note on bundle size: Workers have a gzip-compressed size limit of 3 MiB on the free plan, 10 MiB on paid. The gzip number is what counts — an 8 MiB uncompressed bundle often fits the free tier after compression.
Remove export const runtime = "edge" from every route file. This is the most common migration gotcha and the error it produces is not always obvious.
What to add next
Once your app is running on Workers, the Cloudflare ecosystem fills in the gaps that Next.js expects from a platform:
- Cloudflare D1 — SQLite at the edge. Free tier covers most hobby projects. Add a D1 binding in
wrangler.jsoncand query it with Drizzle or raw SQL via the D1 API. - Cloudflare R2 — S3-compatible object storage. No egress fees. Use it for user uploads, ISR cache (as shown above), or static asset offloading.
- Cloudflare KV — low-latency key-value store. Good for feature flags, session tokens, and rate-limit counters.
- Neon — serverless Postgres. When you need a relational DB and D1’s SQLite constraints are too tight, Neon is the natural fit. It’s officially partnered with Cloudflare, has a free tier, and works well with Drizzle ORM and Prisma.
- Turso — if you want SQLite’s simplicity at production scale, Turso adds multi-region replication and branching on top of libSQL. A solid step up from D1 when your data grows past the D1 free limits.
- Upstash — Redis and QStash for rate-limiting, caching layers, or background job queuing. No confirmed affiliate program; included because it’s the common Workers add-on for anything that needs pub/sub or deferred work.
If you’re evaluating Cloudflare Workers against Vercel for your Next.js project, Vercel vs Cloudflare Pages 2026 breaks down the platform tradeoffs in detail.
Sources: Cloudflare Workers Next.js guide · OpenNext Cloudflare docs · @opennextjs/cloudflare GitHub · Cloudflare blog: OpenNext partnership · Cloudflare compatibility flags