Hosting Next.js on Cloudflare

Hosting Next.js on Cloudflare is cheap, performant and easy to setup. Read on and find out how.

Blog Post Image

Hosting Next.js on Cloudflare in 2025

TL;DR: If you’re starting fresh, deploy Next.js to Cloudflare Workers using the OpenNext adapter. If you already run on Cloudflare Pages + next‑on‑pages, that’s fine—just know you’re on the Edge runtime with a few trade‑offs (notably ISR regeneration). This post walks through both paths with copy‑pasteable configs, caching, images, CI, and common gotchas.


Why Cloudflare for Next.js?

  • Global performance: requests terminate close to your users on Cloudflare’s edge.
  • Zero‑maintenance infra: no servers to patch, scale, or babysit.
  • Rich platform: KV, R2, D1, Queues, Durable Objects, Images, Analytics—native to your app.

In 2025, Cloudflare’s Next.js story has two mature tracks:

  • Workers + OpenNext (@opennextjs/cloudflare) → Node.js runtime support, strong feature coverage (SSR, ISR, Middleware, Route Handlers, Server Actions, PPR*), great for most apps.
  • Pages + next‑on‑pages (@cloudflare/next-on-pages) → Edge runtime only, ideal for edge‑friendly apps and simple SSR, with caveats around ISR regeneration and some Node APIs.

*PPR is still experimental in Next.js—treat it as such.


Decision guide

Use this if…Choose
You want maximum Next.js compatibility (Node runtime features, most libraries)Workers + OpenNext
You rely on ISR (on‑demand revalidation)Workers + OpenNext
You’re already on Pages and your routes all run on EdgePages + next‑on‑pages
You prefer branch/PR previews tied to your repo with minimal setupPages (works with both, but next‑on‑pages is one command)

Prerequisites

This guide assumes you have some basic prerequisites:

  • Cloudflare account (Free is fine). You’ll log in once from the CLI (wrangler login). If you plan to use KV/R2/D1 or larger script sizes, add billing later.
  • Node.js 18.17+ on your dev machine. Next.js v14+ requires ≥18.17 (v15 is happy on 18 or 20). Use nvm/Volta to manage versions.
  • Wrangler CLI. Install and authenticate; Wrangler is the deployment tool for Workers & Pages.
  • A Next.js app (14 or 15). Nothing special here—just your repo.
  • (Optional) A GitHub/GitLab repo if you want automatic preview deploys (especially with Pages).

  1. Scaffold / migrate
# new project
npm create cloudflare@latest -- my-next-app --framework=next

# or add to an existing Next.js app
npm i @opennextjs/cloudflare@latest -D
npm i -D wrangler@latest
  1. wrangler.toml (Workers config)
name = "my-app"
main = ".open-next/worker.js"
compatibility_date = "2025-03-25"
compatibility_flags = ["nodejs_compat"]

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

# Example bindings
# [[kv_namespaces]]
# binding = "CACHE"
# id = "<kv-id>"

# [[r2_buckets]]
# binding = "FILES"
# bucket_name = "my-bucket"

# [[d1_databases]]
# binding = "DB"
# database_name = "my-db"
# database_id = "<d1-id>"
  1. open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";

export default defineCloudflareConfig({
  // tune cache/backends here later if needed
});
  1. package.json scripts
{
  "scripts": {
    "dev": "next dev",
    "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
    "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
    "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
  }
}
  1. Local dev that can talk to Cloudflare services

You get the best DX with next dev. If you need to hit KV/R2/D1 during dev, initialize the adapter once from your Next config:

// next.config.ts
import type { NextConfig } from "next";
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";

const nextConfig: NextConfig = {
  /* your options */
};
export default nextConfig;

// enable Cloudflare bindings for `next dev`
initOpenNextCloudflareForDev();
  1. Bindings in your code (Workers)
// app/api/example/route.ts
import { getCloudflareContext } from "@opennextjs/cloudflare";

export async function GET() {
  const { env } = getCloudflareContext();
  const value = await env.CACHE.get("foo");
  return new Response(value ?? "hello");
}
  1. Environment variables & secrets
  • Local: put them in .env or .dev.vars (pick one). Example .env:

    API_BASE_URL="https://api.example.com"
    JWT_SECRET="dev-only"
  • Production: set Secrets / Vars in the Cloudflare dashboard or via wrangler secret put / [vars].

  • In Workers code, read them from env.MY_VAR (via getCloudflareContext().env). In Node‑land libraries, process.env.X also works thanks to the Node compatibility flag.

  1. Deploy
npm run preview   # run on workerd locally for integration tests
npm run deploy    # ships to *.workers.dev or your custom domain

Path B — Next.js on Cloudflare Pages (Edge runtime)

Not covered in this post, I may cover in the future.


Image optimization the Cloudflare way

Use the built‑in <Image /> with a custom loader that targets Cloudflare Images:

// image-loader.ts
import type { ImageLoaderProps } from "next/image";

const normalizeSrc = (src: string) =>
  src.startsWith("/") ? src.slice(1) : src;
export default function cloudflareLoader({
  src,
  width,
  quality,
}: ImageLoaderProps) {
  if (process.env.NODE_ENV === "development") return src; // serve originals in dev
  const params = [`width=${width}`];
  if (quality) params.push(`quality=${quality}`);
  return `/cdn-cgi/image/${params.join(",")}/${normalizeSrc(src)}`;
}
// next.config.ts
export default {
  images: { loader: "custom", loaderFile: "./image-loader.ts" },
};

If you host originals in R2, restrict transformations to your bucket origin.


Caching & data revalidation

  • Workers + OpenNext: supports SSR, SSG, ISR and on‑demand revalidation APIs. Prefer tag‑based or path‑based revalidation in server code. Pair with KV/R2 if you need global, cross‑region data cache.
  • Pages + next‑on‑pages: default is Cache API (regional). For global reads, wire Workers KV as the data cache. Expect eventual consistency (~tens of seconds) with KV.

Quick patterns

// app/actions/revalidate.ts
"use server";
import { revalidatePath } from "next/cache";

export async function refreshHome() {
  revalidatePath("/");
}
// Force SSR for data that changes frequently
export const dynamic = "force-dynamic";

Storage & platform bindings

Examples (both tracks):

// R2 for uploads
const r2 = getEnv().FILES; // R2 bucket binding
await r2.put(`uploads/${key}`, fileStream);

// D1 for SQL
const db = getEnv().DB; // D1 binding
const { results } = await db.prepare("SELECT * FROM posts LIMIT 10").all();

// KV as a fast cache
await getEnv().CACHE.put("key", JSON.stringify({ v: 1 }), {
  expirationTtl: 60,
});

function getEnv() {
  try {
    // Workers path
    return require("@opennextjs/cloudflare").getCloudflareContext().env;
  } catch {
    // Pages path
    return require("@cloudflare/next-on-pages").getRequestContext().env;
  }
}

CI/CD options

  • Workers: keep using the scripts above in GitHub Actions (or Cloudflare Workers Builds). You can preview with opennextjs-cloudflare preview in CI.
  • Pages: connect the repo to Pages for automatic preview deploys on every PR. Or run wrangler pages deploy from CI.

Monitoring & analytics

  • Cloudflare Logs (Workers) and Real‑time logs for debugging.
  • Cloudflare Web Analytics (drop‑in script) if you want simple, cookieless page analytics.

Common pitfalls & fixes

  1. Node vs Edge runtime confusion
    If a route or a dependency needs Node APIs, prefer Workers + OpenNext. On Pages, mark server routes with export const runtime = 'edge' and avoid Node‑only modules.

  2. Bundle size limits
    Workers enforce compressed script size limits (larger on paid plans). If you hit them:

    • transpilePackages only what you must; avoid bundling the universe.
    • Keep heavy libraries in the client (code‑split), or move work to Durable Objects/Queues.
    • Inspect bundle output; exclude locales (e.g. dayjs over moment), trim icons, remove unused MDX plugins.
  3. ISR expectations
    On Pages, ISR serves static fallbacks only—no background regeneration. Use SSR for frequently changing content or deploy those routes on Workers.

  4. Images look unoptimized
    Ensure you’re using the custom Cloudflare Images loader in production and that your origin allow‑list is correct. During next dev, that loader returns the original URL by design.

  5. Env variables not showing in dev
    Use either .env or .dev.vars (don’t mix), and restart dev after changes. In production, set Vars/Secrets in Cloudflare; don’t bake secrets into wrangler.toml.

  6. MDX & Rust (mdx‑rs) quirks
    If you’ve enabled mdxRs: true in Next, some plugins/pipelines behave differently than the old JS loader. For hosting this mostly matters when your build output changes size/structure—if you hit bundle limits or odd transformations, consider toggling mdxRs off for now or simplifying the MDX pipeline. I’ll cover MDX rendering deep‑dive separately.


Cheatsheet

  • New project? Workers + OpenNext.
  • Need Node.js libs or on‑demand ISR? Workers + OpenNext.
  • Pure Edge and simple SSR? Pages + next‑on‑pages.
  • Images? Use Cloudflare Images + custom loader.
  • Cache? KV for global, Cache API for regional, tune revalidation per route.

Appendix: minimal templates

Workers + OpenNext

npm create cloudflare@latest -- my-next-app --framework=next
cd my-next-app
npm i -D @opennextjs/cloudflare wrangler
# add wrangler.toml, open-next.config.ts, scripts (see above)

Pages + next‑on‑pages

npm create cloudflare@latest -- my-next-app --framework=next --platform=pages
cd my-next-app
npm i -D @cloudflare/next-on-pages wrangler
# add wrangler.toml, next.config.mjs, scripts (see above)