Deploy Next.js to Cloudflare Workers via the OpenNext adapter (@opennextjs/cloudflare). Use for SSR/ISR/SSG/App or Pages Router, getCloudflareContext, bindings (D1/R2/KV/AI/Hyperdrive), caching tiers, skew protection, multi-worker, custom worker, env vars, or worker size/runtime/keep_names/FinalizationRegistry/connection-scoping errors.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Deploy Next.js to Cloudflare Workers via the OpenNext adapter (@opennextjs/cloudflare). Use for SSR/ISR/SSG/App or Pages Router, getCloudflareContext, bindings (D1/R2/KV/AI/Hyperdrive), caching tiers, skew protection, multi-worker, custom worker, env vars, or worker size/runtime/keep_names/FinalizationRegistry/connection-scoping errors.
Deploy Next.js applications to Cloudflare Workers using the OpenNext adapter (@opennextjs/cloudflare). The adapter takes a standard Next.js build, runs package.json build script, then transforms the output to run on the Workers runtime using the Node.js compatibility layer (nodejs_compat) — not the Edge runtime.
Critical Requirements (get these wrong and the build/runtime fails)
Requirement
Value
Why
Runtime
Node.js (default). Remove every export const runtime = "edge";
Edge runtime is unsupported; OpenNext uses nodejs_compat.
Older dates break FinalizationRegistry, DOs, and more.
Wrangler
≥ 3.99.0 to deploy; ≥ 4.13.0 for keep_names; ≥ 4.36.0 for stable remote bindings
Feature gates in the docs.
Next.js
v16 all minors/patches supported; latest minors of v14 and v15; v14 dropped Q1 2026
Stated on the overview page.
Worker size (gzip)
3 MiB Free / 10 MiB Paid (compressed only)
Hard Cloudflare limits.
Windows: not fully guaranteed (Next.js tooling issues). Use WSL, a Linux VM, or Linux/macOS CI. See known issue #1305.
Disambiguation: this skill vs nextjs
nextjs skill → framework/App Router/Server Components/Cache Components patterns, any platform (Vercel, self-hosted, ...). Use for async params, migration, .
proxy.ts
"use cache"
THIS skill (cloudflare-nextjs) → deploying Next.js to Workers via the OpenNext adapter: wrangler.jsonc, open-next.config.ts, getCloudflareContext, caching tiers, bindings, skew protection, multi-worker, the Workers-specific errors.
proxy.ts caveat (Next 16): Next 16 renamed middleware.ts → proxy.ts, but @opennextjs/cloudflare does not recognize proxy.ts yet (issue #1277) — on Cloudflare, keep using middleware.ts. This is the one place the nextjs skill's guidance does NOT apply here.
C3 scaffolds a Next.js app, installs @opennextjs/cloudflare, creates wrangler.jsonc + open-next.config.ts + .dev.vars, wires package.json scripts, and (if R2 is enabled) creates an R2 bucket for caching.
Existing Next.js project (one command)
npx @opennextjs/cloudflare migrate
migrate automates: install adapter + wrangler, create wrangler.jsonc/open-next.config.ts/.dev.vars, update scripts, add public/_headers, add .open-next to .gitignore, wire initOpenNextCloudflareForDev() into next.config.ts, and create+configure an R2 cache bucket (only if R2 is enabled on the account).
Then create the three files (see references/wrangler.jsonc, references/open-next.config.ts, references/package.json) and add the dev/preview/deploy/upload/cf-typegen scripts. Pin adapter versions and audit before upgrading — see the dependency-upgrade skill.
The four scripts
// package.json{"dev":"next dev",// fast HMR via Next dev server"preview":"opennextjs-cloudflare build && opennextjs-cloudflare preview",// build + run in workerd locally"deploy":"opennextjs-cloudflare build && opennextjs-cloudflare deploy",// build + serve immediately"upload":"opennextjs-cloudflare build && opennextjs-cloudflare upload",// build + upload a version (gradual rollout)"cf-typegen":"wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"}
dev — fastest feedback loop; add initOpenNextCloudflareForDev() to next.config.ts so getCloudflareContext() works locally with simulated/remote bindings.
preview — runs in the actual Workers runtime (not Node). Always run before deploy to catch runtime-only issues.
deploy — populates the remote cache, then wrangler deploy. App serves immediately.
upload — populates remote cache, then wrangler versions upload. Does NOT serve automatically; for gradual deployments.
build, preview, deploy, upload all implicitly call populateCache — you do not need to run it manually.
TypeScript types:npm run cf-typegen generates cloudflare-env.d.ts (re-run after any binding change).
Remote bindings (local dev → real resources): stabilized in Wrangler 4.36.0. On older wrangler, enable via initOpenNextCloudflareForDev({ experimental: { remoteBindings: true } }) and use the experimental_remote (not remote) key on binding options. Note: remote bindings are also used during build.
Full patterns (D1/R2/KV/AI/Hyperdrive, Drizzle, Prisma, Stripe) → references/bindings-and-services.md.
Caching — three components, three tiers
OpenNext's cache has three parts: Incremental Cache (storage), Queue (dedupe/revalidate), Tag Cache (on-demand revalidateTag/revalidatePath).
Reserved binding names (do not reuse): ASSETS, WORKER_SELF_REFERENCE, NEXT_INC_CACHE_R2_BUCKET, NEXT_CACHE_DO_QUEUE, NEXT_TAG_CACHE_D1, NEXT_TAG_CACHE_DO_SHARDED, NEXT_CACHE_DO_PURGE, IMAGES.
Avoid Workers KV for incremental cache — eventually consistent, can persist stale data indefinitely.
Cache interception + PPR: incompatible today; cache interception is NOT enabled by default and does not work with PPR.
On-demand revalidation requires both a Tag Cache and the Cache Purge component (cache purge only works on a zone/custom domain; needs CACHE_PURGE_API_TOKEN + CACHE_PURGE_ZONE_ID secrets).
Pages Routerres.revalidate requires a self-reference service binding named WORKER_SELF_REFERENCE.
Headers caveat: the Worker does not run in front of static assets, so next.config.tsheaders() for public/ and immutable build files do not apply. Use public/_headers.
Deep dive (all options, env vars, regional modes, migration from 0.6) → references/caching.md and references/known-issues.md.
Common Integrations (condensed — full patterns in references)
Drizzle + D1/Hyperdrive/PG, Prisma + D1/PG/Hyperdrive — request-scoped clients via cache() from react; maxUses: 1 on PG pools; getCloudflareContext({ async: true }) for ISR/SSG; Prisma needs previewFeatures = ["driverAdapters"], no output dir in schema.prisma, and serverExternalPackages: ["@prisma/client", ".prisma/client"]. → references/bindings-and-services.md
Stripe — Workers have no node:https; pass httpClient: Stripe.createFetchHttpClient(). → references/bindings-and-services.md
Image optimization — images.binding: "IMAGES" in wrangler.jsonc, or a custom loader (/cdn-cgi/image/...) for zones. minimumCacheTTL and dangerouslyAllowLocalIP are not supported; custom loader bypasses middleware and ignores remotePatterns. → references/advanced.md
Env vars — use Next.js .env files (not just .dev.vars); NEXTJS_ENV in .dev.vars selects the env; --keep-vars on deploy; secrets are write-only. → references/dev-deploy-and-env.md
Custom worker (add scheduled, Durable Object exports) — point main at your worker that re-exports the generated fetch handler. → references/advanced.md
Multi-worker (split middleware from server) — reduces per-worker memory + cold starts; incompatible with preview URLs, skew protection, and @opennextjs/cloudflare deploy. → references/advanced.md
Skew protection (preview-URL-based version matching) — cloudflare.skewProtection.enabled, run_worker_first: true, getDeploymentId(), env vars CF_WORKER_NAME/CF_PREVIEW_DOMAIN/CF_WORKERS_SCRIPTS_API_TOKEN/CF_ACCOUNT_ID. Disabled for Workers with a Durable Object (move DOs to a separate worker). → references/advanced.md
Top Errors (full catalog → references/error-catalog-extended.md)
1. Worker size limit exceeded
"Your Worker exceeded the size limit of 3 MiB" (Free) / "10 MiB" (Paid). Only gzip size counts. Free → upgrade to Paid. Paid → analyze bundle: npx @opennextjs/cloudflare build, then inspect .open-next/server-functions/default/handler.mjs.meta.json (visualize with ESBuild Bundle Analyzer); remove unused deps, use dynamic imports.
2. Cannot perform I/O on behalf of a different request
Global DB client (e.g. postgres, pg Pool) reused across requests. Create the client inside the request handler (or use cache() from react), and maxUses: 1 for PG pools.
3. NPM package import / "Could not resolve <package>"
Enable nodejs_compat, ensure compatibility_date ≥ 2024-09-23. Some packages ship a workerd export — add them to serverExternalPackages in next.config.ts (e.g. @prisma/client, .prisma/client, postgres, jose, react-textarea-autosize, @libsql/isomorphic-ws). Or set .env: WRANGLER_BUILD_CONDITIONS="" + WRANGLER_BUILD_PLATFORM="node".
5. Failed to load chunk server/chunks/ssr/<name>.js
Outdated adapter with Turbopack builds. Upgrade @opennextjs/cloudflare to latest, or switch to webpack (next build without --turbo).
6. ReferenceError: FinalizationRegistry is not defined
compatibility_date too old. Set "compatibility_date": "2025-05-05" (or later) in wrangler.jsonc.
7. Uncaught ReferenceError: __name is not defined
Wrangler's esbuild keep-names injects __name into generated script strings that some libs (e.g. next-themes) eval at runtime. Set "keep_names": false in wrangler.jsonc (requires Wrangler ≥ 4.13.0). You lose original function names in debugging.
8. "Failed to send request to R2 worker" / 403 during populateCache remote
Account protected by Cloudflare Access blocks the open-next-cache-populate helper worker. Do not create a separate Access app for it; add a Service Auth policy (Include = Any Access Service Token) to the existing app covering *.<account>.workers.dev, create a service token, and export CLOUDFLARE_ACCESS_CLIENT_ID / CLOUDFLARE_ACCESS_CLIENT_SECRET.
Known Open Bugs (live tracker)
Always check the issue tracker — these are recurring at the time of writing: