If you're running Next.js on Vercel and worried about costs as traffic grows — or simply want to experience true edge computing — Cloudflare Workers deserves serious consideration.
This guide walks you through deploying Next.js 15 on Cloudflare Workers using OpenNext, from initial setup to CI/CD in production.
Why Cloudflare Workers Over Vercel?
Cloudflare Workers runs your code across 300+ global edge locations — not from a single fixed region. Users in Hanoi, Berlin, or São Paulo all get responses from the nearest node.
| Cloudflare Workers | Vercel |
|---|
| Latency | ~10-50ms (edge) | ~50-200ms (regional) |
| Cold start | ~0ms (V8 isolates) | 100-300ms (Node.js) |
| Free tier | 100K req/day | 100GB bandwidth/month |
| Pricing | $5/10M requests | $0.40/GB after free |
| Bundle limit | 10MB | 50MB |
- Zero cold starts: V8 isolates spin up nearly instantly, unlike Node.js containers
- Predictable costs: Pay per request rather than per bandwidth
- No vendor lock-in: Open-source adapter, standard Web APIs
- Great DX: Wrangler CLI for local dev and production deploys
When to choose Workers? When you need the lowest possible latency, have bursty traffic patterns, or want to escape Vercel's pricing model.
Prerequisites
- Node.js 20+
- Cloudflare account (free at cloudflare.com)
- Next.js 15 project
- (Optional) Neon PostgreSQL for SSR with dynamic data
Step 1: Install OpenNext and Wrangler
OpenNext is an open-source adapter that makes Next.js run on any serverless platform, including Cloudflare Workers.
npm install @opennextjs/cloudflare
npm install --save-dev wrangler
Authenticate with Cloudflare:
npx wrangler login
Create wrangler.jsonc at the project root:
{
"name": "my-nextjs-app",
"main": ".open-next/worker.js",
"compatibility_date": "2024-12-01",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"kv_namespaces": [
{
"binding": "CACHE",
"id": "YOUR_KV_NAMESPACE_ID"
}
],
"vars": {
"NEXT_PUBLIC_APP_URL": "https://myapp.com"
}
}
Create the KV namespace for caching:
npx wrangler kv namespace create CACHE
Step 3: Environment Variables
Never commit secrets to wrangler.jsonc. Use Wrangler secrets instead:
npx wrangler secret put DATABASE_URL
npx wrangler secret put NEXTAUTH_SECRET
npx wrangler secret list
For local development, use .env.local:
DATABASE_URL=postgresql://user:[email protected]/db?sslmode=require
NEXTAUTH_SECRET=your-dev-secret
NEXT_PUBLIC_APP_URL=http://localhost:3000
Step 4: Database — Neon PostgreSQL
Cloudflare Workers cannot use TCP connections. You must use Neon's HTTP-based driver:
npm install @neondatabase/serverless
Basic connection:
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
const users = await sql`SELECT id, name FROM users LIMIT 10`;
With Drizzle ORM (recommended for production):
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Important: If you're using pg (node-postgres) with new Pool(...), you need to migrate to @neondatabase/serverless. Workers don't have TCP sockets.
Step 5: Local Development with Workers Runtime
Don't just run npm run dev. Test in the actual V8 isolate:
npm run build
npx @opennextjs/cloudflare build
npx wrangler dev
This is critical — it catches runtime errors that don't appear in plain Node.js (e.g., TCP connections, missing Node.js APIs).
Step 6: Deploy to Production
npx @opennextjs/cloudflare build
npx wrangler deploy
Output after a successful deploy:
✅ Deployed my-nextjs-app
https://my-nextjs-app.your-username.workers.dev
Step 7: Custom Domain
Your domain must be managed in Cloudflare DNS:
npx wrangler domains add myapp.com
Or via wrangler.jsonc:
{
"routes": [
{
"pattern": "myapp.com/*",
"zone_name": "myapp.com"
}
]
}
Step 8: CI/CD with GitHub Actions
Auto-deploy on every push to main:
name: Deploy to Cloudflare Workers
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 wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Create your token: Cloudflare Dashboard → My Profile → API Tokens → Create Token → use "Edit Cloudflare Workers" template. Add it as a GitHub Secret named CLOUDFLARE_API_TOKEN.
1. Aggressive caching
export async function GET() {
const data = await getProducts();
return Response.json(data, {
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
},
});
}
2. Cloudflare KV for session data
const cached = await env.CACHE.get(`session:${userId}`);
if (!cached) {
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
await env.CACHE.put(`session:${userId}`, JSON.stringify(user), {
expirationTtl: 3600,
});
}
3. Keep bundle under 10MB
- Use named imports — avoid importing entire libraries
- Analyze with
@next/bundle-analyzer
- Use
next/dynamic for heavy components
4. Streaming SSR for large pages
import { Suspense } from "react";
export default function DashboardPage() {
return (
<Suspense fallback={<Skeleton />}>
<DataHeavyComponent />
</Suspense>
);
}
Common Issues
| Error | Cause | Fix |
|---|
| "Cannot use TCP connections" | Using pg Pool | Switch to @neondatabase/serverless |
| "Script size exceeds 10MB" | Bundle too large | Run wrangler deploy --dry-run, use dynamic imports |
| "node:crypto not found" | Missing flag | Add "nodejs_compat" to compatibility_flags |
| Middleware not working | Wrong config | Check open-next.config.ts exports |
| Build ok but runtime error | Incompatible Node.js API | Test with npx wrangler dev before deploying |
Building an MVP with Next.js? Check out why Next.js + TypeScript + Postgres is the best MVP stack — the Cloudflare Workers setup works seamlessly with this stack. Need to add a data pipeline? See our guide on building real-time data pipelines with PostgreSQL.