· 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

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 produces
  • compatibility_date: must be 2024-09-23 or later — this date activates the full Node.js compatibility layer; 2024-12-30 is a safe, stable choice
  • nodejs_compat: required flag; without it, most Next.js server code will fail at runtime
  • global_fetch_strictly_public: required for correct fetch behavior from inside the Worker
  • assets binding: this is what lets Workers serve static files — previously a Pages-only feature, now available to Workers via this binding
  • WORKER_SELF_REFERENCE service binding: required for the adapter’s internal routing; missing it causes 500 errors on workers.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” template
  • CLOUDFLARE_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

FeatureStatusNotes
App RouterFull support
Pages RouterFull support
Server Components
SSG
SSR
ISRRequires R2 bucket + r2IncrementalCache override
Server Actions
Middleware
Image OptimizationRequires 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 devUse 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.jsonc and 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