After you complete this article, you will have a solid understanding of:
Why deploying Next.js SSR on Cloudflare is different from traditional hosting
The real difference between Cloudflare Pages with next-on-pages and OpenNext adapter
How the free tier's 100,000 daily requests can handle production traffic
Common deployment pitfalls that will waste hours of debugging
Which adapter to choose for your specific use case
Have You Ever Seen This Error When Deploying to Cloudflare?
If you've tried deploying a Next.js app to Cloudflare Pages, you've probably encountered this frustrating message:
Error: Dynamic server usage: Page couldn't be rendered statically because it used `cookies`.
Or even worse:
Error: The edge runtime does not support Node.js 'fs' module.
You can use 'fs' module only in Node.js runtime.
And then you wonder: "But I thought Cloudflare supports Next.js SSR now?"

Let me clear up this confusion once and for all.
The Two Ways to Deploy Next.js on Cloudflare
There are two completely different approaches to deploying Next.js on Cloudflare, and choosing the wrong one will cause endless headaches.
Option 1: @cloudflare/next-on-pages (The Limited One)
This was the original way, and it comes with a massive limitation:
export const runtime = 'edge';
export default async function Page() {
const fs = require('fs');
const bcrypt = require('bcrypt');
const response = await fetch('https://api.example.com');
return <div>Limited to Edge Runtime</div>;
}
Option 2: @opennextjs/cloudflare (The Game Changer)
Released in 2024 and now in v1.0-beta, this adapter supports the Node.js runtime:
export default async function Page() {
const fs = require('fs');
const crypto = require('crypto');
const data = await prisma.user.findMany();
return <div>Full Node.js support!</div>;
}
Setting Up Next.js SSR with OpenNext (The Right Way)
Let's deploy a real Next.js app with full SSR support on Cloudflare.
Option 1: Using Cloudflare CLI (Recommended)
npm create cloudflare@latest my-nextjs-app -- \
--framework=next --platform=workers
npm run deploy
Ready to see this in action? Check out the complete working example with full source code, deployment configuration, and step-by-step setup instructions here
Migrating Your Existing Next.js App to Cloudflare
If you already have a Next.js app running on Vercel, AWS, or anywhere else, here's how to migrate it to Cloudflare.
Step 1: Check Your Current Setup
First, identify which features your app uses:
npm list next
grep -r "export const runtime" .
grep -r "getServerSideProps" .
grep -r "getStaticProps" .
grep -r "app/api" .
Step 2: Choose Your Migration Path
If your app uses Vercel:
npx diverce migrate
For manual migration:
npm install -D @opennextjs/cloudflare wrangler
npm uninstall @vercel/analytics @vercel/og
Step 3: Update Your Configuration
Remove edge runtime declarations:
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
Update your next.config.js:
const { setupDevPlatform } = require('@cloudflare/next-on-pages/next-dev');
if (process.env.NODE_ENV === 'development') {
await setupDevPlatform();
}
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();
const nextConfig = {
images: {
domains: ['example.com'],
},
};
module.exports = nextConfig;
If you're using Vercel KV:
import { kv } from '@vercel/kv';
await kv.set('key', 'value');
export async function GET(request: Request, env: Env) {
await env.MY_KV.put('key', 'value');
return new Response('Saved!');
}
If you're using Vercel Postgres:
import { sql } from '@vercel/postgres';
const { rows } = await sql`SELECT * FROM users`;
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const { rows } = await pool.query('SELECT * FROM users');
If you're using Vercel Edge Config:
import { get } from '@vercel/edge-config';
const value = await get('featureFlag');
export async function GET(request: Request, env: Env) {
const value = await env.CONFIG_KV.get('featureFlag');
return Response.json({ value });
}
Step 5: Update Environment Variables
Create a .dev.vars file for local development:
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
NEXTAUTH_SECRET=your-secret-key
NEXT_PUBLIC_API_URL=https://api.example.com
Add bindings to wrangler.toml:
Before that
Optional: create the R2 bucket for the cache If you added R2 incremental cache in your open-next.config.ts.
Run this command to create it:
npx wrangler r2 bucket create your-bucket-name
name = "my-nextjs-app"
main = ".open-next/worker.js"
compatibility_date = "2025-07-27"
compatibility_flags = ["nodejs_compat"]
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
[[r2_buckets]]
binding = "NEXT_INC_CACHE_R2_BUCKET"
bucket_name = "my-bucket-name"
Now let deploy :
//Run the command below
npm run deploy
If everything works well, your Next.js app will be live on Cloudflare's global network within seconds, accessible via a *.workers.dev URL or your custom domain, serving from 280+ locations worldwide with automatic SSL.
Common Migration Issues
Issue 1: Dynamic imports failing
const MyComponent = dynamic(() => import('./MyComponent'), {
ssr: false
});
const MyComponent = dynamic(
() => import('./MyComponent'),
{
ssr: false,
loading: () => <div>Loading...</div>
}
);
Issue 2: File system access
import fs from 'fs';
const data = fs.readFileSync('./data.json');
import data from './data.json';
const response = await fetch('/data.json');
const data = await response.json();
Issue 3: Image optimization
module.exports = {
images: {
loader: 'custom',
loaderFile: './image-loader.js',
},
};
export default function cloudflareLoader({ src, width, quality }) {
const params = [`width=${width}`];
if (quality) {
params.push(`quality=${quality}`);
}
const paramsString = params.join(',');
return `/cdn-cgi/image/${paramsString}/${src}`;
}
Step 7: Test Your Migration
npm run preview
curl http://localhost:8787/api/health
curl http://localhost:8787/dashboard
The Free Tier: More Powerful Than You Think
Cloudflare's free tier includes:
100,000 requests per day (resets at midnight UTC)
10ms CPU time per request (plenty for SSR)
128MB memory per Worker
Unlimited static asset requests ๐
Real-World Capacity Example
Advanced Features That Actually Work
1. API Routes with Full Node.js
import { createHash } from 'crypto';
import { headers } from 'next/headers';
export async function POST(request: Request) {
const body = await request.json();
const hash = createHash('sha256')
.update(body.data)
.digest('hex');
const headersList = headers();
const userAgent = headersList.get('user-agent');
const result = await processDataWithNodeAPIs(body);
return Response.json({
hash,
processed: result,
userAgent
});
}
2. Middleware That Scales
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
const response = NextResponse.next();
response.headers.set('x-user-country', country);
if (country === 'CN' && request.nextUrl.pathname === '/') {
return NextResponse.redirect(new URL('/cn', request.url));
}
return response;
}
export const config = {
matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
};
3. ISR That Actually Works
export async function generateStaticParams() {
return [
{ slug: 'getting-started' },
{ slug: 'advanced-features' }
];
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.blog.com/posts/${params.slug}`, {
next: { revalidate: 3600 }
});
return <article>{}</article>;
}
Production Deployment Checklist
Before deploying to production, ensure:
โ
nodejs_compat flag is set
โ
Environment variables are configured in Cloudflare dashboard
โ
R2 bucket is created for caching (optional)
โ
Custom domain is configured
โ
Preview deployments are working
Deploy Command
npm run deploy -- --env production
npm run deploy -- --env preview
When to Use Which Approach?
Use @cloudflare/next-on-pages when:
Your app only uses Web APIs
You need the absolute fastest cold starts
You're building a simple marketing site
Use @opennextjs/cloudflare when:
You need Node.js APIs (crypto, fs, etc.)
You're using Prisma or other Node.js ORMs
You have existing Next.js apps to migrate
You want the full Next.js feature set
The Future is Here
With OpenNext adapter reaching v1.0, deploying production Next.js apps on Cloudflare is finally practical. You get:
True SSR with full Node.js support
Global edge deployment from 280+ locations
Generous free tier for getting started
Seamless scaling when you grow
Remember: You're not choosing between features and performance anymore. You can have both.
Was this article helpful for you? If so, let me know what you think in the comment section.