Deploying Next.js SSR on Cloudflare: The Complete Guide to OpenNext vs next-on-pages

Yusuf AdeyemoYusuf Adeyemo
8 min read

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?"

A confused developer looking at two doors - one labeled "Edge Runtime" with a โš ๏ธ warning sign, another labeled "Node.js Runtime" with a โœ… check mark

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:

// Every server component needs this ๐Ÿ‘‡
export const runtime = 'edge';

export default async function Page() {
  // โŒ This will fail!
  const fs = require('fs');

  // โŒ This will also fail!
  const bcrypt = require('bcrypt');

  // โœ… Only Web APIs work
  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:

// No runtime declaration needed! ๐ŸŽ‰

export default async function Page() {
  // โœ… This works now!
  const fs = require('fs');

  // โœ… This works too!
  const crypto = require('crypto');

  // โœ… Even database connections work
  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.

# Create new Cloudflare project
npm create cloudflare@latest my-nextjs-app -- \
  --framework=next --platform=workers

# Deploy
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:

# Check your Next.js version
npm list next

# Look for these in your code:
grep -r "export const runtime" . # Edge runtime declarations
grep -r "getServerSideProps" .   # SSR pages
grep -r "getStaticProps" .        # SSG pages
grep -r "app/api" .               # API routes

Step 2: Choose Your Migration Path

If your app uses Vercel:

# Use Diverce for automatic migration!
npx diverce migrate

# This tool automatically:
# - Adds OpenNext to your project
# - Updates your configuration
# - Creates a PR with all changes

For manual migration:

# Install the OpenNext adapter
npm install -D @opennextjs/cloudflare wrangler

# Remove any Vercel-specific packages
npm uninstall @vercel/analytics @vercel/og

Step 3: Update Your Configuration

Remove edge runtime declarations:

// โŒ Remove these from all your files
export const runtime = 'edge';
export const dynamic = 'force-dynamic';

// โœ… OpenNext handles this automatically
// Just delete these lines!

Update your next.config.js:

// next.config.js
const { setupDevPlatform } = require('@cloudflare/next-on-pages/next-dev');

// โŒ Remove this if you have it
if (process.env.NODE_ENV === 'development') {
  await setupDevPlatform();
}

// โœ… Add this instead
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();

const nextConfig = {
  // Your existing config stays the same!
  images: {
    domains: ['example.com'],
  },
  // Remove any Vercel-specific settings
  // outputFileTracing: false, โŒ
};

module.exports = nextConfig;

Step 4: Handle Platform-Specific Code

If you're using Vercel KV:

// โŒ Before (Vercel KV)
import { kv } from '@vercel/kv';
await kv.set('key', 'value');

// โœ… After (Cloudflare KV)
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:

// โŒ Before (Vercel Postgres)
import { sql } from '@vercel/postgres';
const { rows } = await sql`SELECT * FROM users`;

// โœ… After (Any PostgreSQL client works!)
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:

// โŒ Before (Vercel Edge Config)
import { get } from '@vercel/edge-config';
const value = await get('featureFlag');

// โœ… After (Cloudflare KV or D1)
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:

# .dev.vars (like .env.local but for Cloudflare)
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"]


# Static assets configuration
[assets]
directory = ".open-next/assets"
binding = "ASSETS"


# R2 Buckets
[[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

// โŒ This might fail
const MyComponent = dynamic(() => import('./MyComponent'), {
  ssr: false
});

// โœ… Ensure proper configuration
const MyComponent = dynamic(
  () => import('./MyComponent'),
  { 
    ssr: false,
    loading: () => <div>Loading...</div>
  }
);

Issue 2: File system access

// โŒ This won't work in production
import fs from 'fs';
const data = fs.readFileSync('./data.json');

// โœ… Use static imports or fetch
import data from './data.json';
// OR
const response = await fetch('/data.json');
const data = await response.json();

Issue 3: Image optimization

// โœ… Next/Image works but needs configuration
// In next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './image-loader.js',
  },
};

// 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

# 1. Build and preview locally
npm run preview

# 2. Check for errors
# Common errors and fixes:

# "Cannot find module 'fs'"
# โ†’ Remove file system operations

# "window is not defined"
# โ†’ Wrap in useEffect or check typeof window

# "Module not found: Can't resolve 'encoding'"
# โ†’ Add to externals in next.config.js

# 3. Test all your routes
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

// Let's calculate what 100k requests means:

// Average SSR page: ~50ms total time (including I/O)
// CPU time used: ~5-10ms
// Memory used: ~30-50MB

// Daily capacity on free tier:
// - 100,000 page views
// - ~4,166 page views per hour
// - ~69 page views per minute

// That's enough for:
// - A blog with 50k daily visitors (2 pages each)
// - A SaaS dashboard with 10k daily active users
// - An e-commerce site with 20k daily shoppers

Advanced Features That Actually Work

1. API Routes with Full Node.js

// app/api/process/route.ts
import { createHash } from 'crypto';
import { headers } from 'next/headers';

export async function POST(request: Request) {
  const body = await request.json();

  // โœ… Node.js crypto works!
  const hash = createHash('sha256')
    .update(body.data)
    .digest('hex');

  // โœ… Headers manipulation
  const headersList = headers();
  const userAgent = headersList.get('user-agent');

  // โœ… Complex processing
  const result = await processDataWithNodeAPIs(body);

  return Response.json({ 
    hash, 
    processed: result,
    userAgent 
  });
}

2. Middleware That Scales

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Runs at the edge for EVERY request
  const country = request.geo?.country || 'US';

  // Add custom headers
  const response = NextResponse.next();
  response.headers.set('x-user-country', country);

  // Redirect based on geo
  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

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  // Pre-build these pages
  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 } // Revalidate every hour
  });

  return <article>{/* Your content */}</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

# Deploy to production
npm run deploy -- --env production

# Deploy preview
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.

0
Subscribe to my newsletter

Read articles from Yusuf Adeyemo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yusuf Adeyemo
Yusuf Adeyemo

Yusuf Adeyemo is a DevOps Engineer from Nigeria. He loves helping startups deliver better software and provide more control over their environment and software development process with the help of modern tools and automation.