Migrate Cloudflare Pages to Workers Guide
Migrate Next.js from Pages (`next-on-pages`) to the Workers-based @opennextjs/cloudflare adapter to unlock Node.js runtime, ISR, PPR, and other modern features.

Why Choose OpenNext.js/Cloudflare?
Previously, we used Next.js with Cloudflare Pages for full-stack development, as it was the officially recommended solution by Cloudflare. However, Cloudflare Pages is better suited for static websites, while Next.js's latest features (e.g., ISR, PPR) require a Node.js-compatible environment. Recently, Cloudflare announced the GA of OpenNext.js/Cloudflare and recommended Pages users migrate to Workers to fully unleash the power of Next.js.
Benefits of Migration
Support for New Features: Runs in Cloudflare Workers with Node.js compatibility, unlocking ISR, PPR, and other features.
Flexible Runtime: OpenNext.js/Cloudflare is an adapter allowing Next.js to run in Node.js environments, rather than Pages' Edge.
Optimized Caching: Supports efficient caching via R2 or KV to boost performance.
Prerequisites
Next.js Runtime: OpenNext.js/Cloudflare supports only Node.js runtime (Edge is not supported).
Next.js Version: Supports Next.js v14 and v15.
Migration Steps
Install Dependencies
npm install @opennextjs/cloudflare@latest
npm install --save-dev wrangler@latestCreate wrangler.jsonc (Optional)
Not required, as @opennextjs/cloudflare auto-generates defaults. But if customizing (e.g., D1, R2, preview), create one:
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "<APP-NAME>",
"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": "<APP-NAME>"
}
],
"r2_buckets": [
// Optional R2 bucket bindings
]
}compatibility_dateis required and must be >= 2024-09-23.mainpoints to build output.open-next/worker.js.
Create open-next.config.ts (Optional)
If not created, opennextjs-cloudflare build will generate a default configuration. It is recommended to create an open-next.config.ts file in the project root directory.
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
});Add .dev.vars
Create a .dev.vars file in root to support local dev/preview:
NEXTJS_ENV=developmentUpdate package.json Scripts
{
"scripts": {
"build": "next build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}Enable Long Cache for Static Files
public/_headers:
/_next/static/*
Cache-Control: public,max-age=31536000,immutableSwitch to Node.js Runtime
Remove all export const runtime = "edge".
Git Ignore Build Artifacts
# .gitignore
.open-nextRemove Cloudflare Pages Dependencies
Uninstall:
@cloudflare/next-on-pages,eslint-plugin-next-on-pagesRemove
setupDevPlatform()innext.config.*Replace
getRequestContext→getCloudflareContextfrom@opennextjs/cloudflare
Caching Architecture (ISR/PPR)
Serverless environments lack persistent file systems. OpenNext.js/Cloudflare uses:
Incremental Cache: Stores page/data cache (R2 or KV)
Queue: Manages revalidation scheduling
Tag Cache: Supports
revalidateTag/revalidatePath
Choose Storage Backend
R2: Strong consistency, suitable for long-term caching
KV: Low latency, ~60s multi-region consistency delay
R2 Setup Example
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
import queueCache from "@opennextjs/cloudflare/overrides/queue/queue-cache";
import doShardedTagCache from "@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache";
export default defineCloudflareConfig({
incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }),
tagCache: doShardedTagCache({ baseShardSize: 12, regionalCache: true }),
queue: queueCache(doQueue, {
regionalCacheTtlSec: 5,
waitForQueueAck: true,
}),
});wrangler.jsonc Bindings
{
"r2_buckets": [
{ "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "<YOUR-R2-BUCKET>" }
],
"durable_objects": {
"bindings": [
{ "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler" },
{ "name": "NEXT_TAG_CACHE_DO_SHARDED", "class_name": "DOShardedTagCache" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"] }
]
}Environment Variables: Build-time vs Runtime
Runtime: Bindings (R2, KV, etc.) defined in
wrangler.jsoncare available at runtime.Build-time: These bindings are not accessible during
next build.
Workarounds:
Shift logic to runtime via
getServerSidePropsorfetchUse placeholder + hydrate on client
Or access via Cloudflare API with token
Local Dev and Verification
npm run preview # Start local preview
npm run deploy # Deploy to Workers
npm run upload # Upload version (no traffic cutover)
npm run cf-typegen # Generate TypeScript bindingsVerification Checklist
compatibility_date >= 2024-09-23,wrangler >= 3.99.0Removed
runtime = "edge"and next-on-pagesnameinwrangler.jsoncmatchesservices.service.open-nextadded to.gitignoreStatic headers configured
Cache behavior works as expected
CI/CD Example (GitHub Actions)
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
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
- run: npx opennextjs-cloudflare build
- run: npx opennextjs-cloudflare upload
- if: github.ref == 'refs/heads/main'
run: npx opennextjs-cloudflare deploy
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}Common Pitfalls
Missing version/date config
Edge runtime leftovers
Mismatched
nameandservices.serviceNo caching backend configured (R2/KV)
Inconsistencies with KV
Incomplete cleanup (e.g.,
setupDevPlatform()remains)Build-time binding access (should use runtime or API)
FAQ
Q: Can I mix Edge and Node routes? A: No. This adapter supports only Node.js runtime. Use Edge only for Middleware if necessary.
Q: How to debug ISR failures?
A: Check DO logs (DOQueueHandler). Verify R2 access. Confirm tagCache initialization.
Q: How to enable geo caching?
A: Use withRegionalCache() and fine-tune regionalCacheTtlSec.
Rollback Strategy
Retain last working
Pages + next-on-pagesdeploymentUpload before deploy to enable safe rollout
Use new namespaces (R2/KV/DO) to prevent pollution
Conclusion
By following these steps, you can migrate your Next.js app from Cloudflare Pages to OpenNext.js/Cloudflare and unlock full Node.js runtime support and advanced caching. Test locally via npm run preview or deploy to production via npm run deploy.
For further assistance, refer to the official OpenNext.js and Cloudflare Workers documentation.

