billing hacks
Cloudflare Deep DiveCaching Differences

Caching on Cloudflare

How Next.js caching translates to Cloudflare's model and ISR alternatives.

Next.js caching was designed for Vercel. On Cloudflare, you need KV, D1, or R2 for durable cache. ISR requires manual implementation.

Next.js caching was designed for Vercel's infrastructure. On Cloudflare, the model is fundamentally different.

The Architecture Difference

VERCEL vs CLOUDFLARE ARCHITECTUREVERCELReqEdgeRegional Cacheshared globallyLambdaOne cache serves all regionsCLOUDFLAREReqEdge PoPsWorkersKV / CacheCache per PoP (200+)Key Difference:Vercel: Global cacheCF: Per-location cache

Cloudflare caches are per-PoP, not global. A cache miss in Tokyo doesn't benefit from a cache hit in London.

ISR: The Big Difference

Next.js ISR on Vercel uses a shared cache. On Cloudflare, you need to implement it differently.

Vercel ISR

// pages/posts/[id].tsx - Just works
export async function getStaticProps({ params }) {
  return {
    props: { post: await getPost(params.id) },
    revalidate: 60, // Regenerate every 60 seconds
  };
}

Cloudflare Alternative

// Using KV for cache with manual revalidation
export async function GET(request: Request, { env }) {
  const cacheKey = `post:${params.id}`;

  // Check KV cache
  const cached = await env.CACHE.get(cacheKey, 'json');
  if (cached && Date.now() < cached.expiresAt) {
    return Response.json(cached.data);
  }

  // Fetch fresh data
  const post = await getPost(params.id);

  // Store in KV with TTL
  await env.CACHE.put(cacheKey, JSON.stringify({
    data: post,
    expiresAt: Date.now() + 60000
  }), {
    expirationTtl: 3600 // KV TTL (longer than stale threshold)
  });

  return Response.json(post);
}

Cache API vs KV

Cloudflare offers two caching options:

FeatureCache APIWorkers KV
ScopePer-PoPGlobal (eventually consistent)
Latency~1ms~50ms reads
ConsistencyNone~60s propagation
Best forHot data, same-regionShared state, infrequent writes

Cache API (Per-PoP)

export async function GET(request: Request) {
  const cache = caches.default;
  const cacheKey = new Request(request.url);

  // Check cache
  let response = await cache.match(cacheKey);
  if (response) return response;

  // Generate response
  response = new Response(JSON.stringify(data), {
    headers: {
      'Cache-Control': 'public, max-age=60',
      'Content-Type': 'application/json'
    }
  });

  // Store in cache (non-blocking)
  request.waitUntil(cache.put(cacheKey, response.clone()));

  return response;
}

Workers KV (Global)

export async function GET(request: Request, { env }) {
  const key = 'global-config';

  // KV is globally distributed
  const config = await env.CONFIG.get(key, 'json');

  return Response.json(config);
}

Static Assets

Static assets work well on Cloudflare Pages:

next.config.js
module.exports = {
  assetPrefix: process.env.NODE_ENV === 'production'
    ? 'https://assets.example.com'
    : undefined,
};

Cloudflare's CDN handles static files with proper cache headers automatically.

Data Cache Replacement

Next.js fetch cache doesn't translate directly:

// ❌ Vercel-style fetch caching
const data = await fetch(url, {
  next: { revalidate: 3600 }
});

// ✅ Cloudflare: Manual caching with Cache API
async function cachedFetch(url: string, ttl: number) {
  const cache = caches.default;
  const cacheKey = new Request(url);

  const cached = await cache.match(cacheKey);
  if (cached) return cached;

  const response = await fetch(url);
  const newResponse = new Response(response.body, {
    headers: {
      ...response.headers,
      'Cache-Control': `public, max-age=${ttl}`
    }
  });

  await cache.put(cacheKey, newResponse.clone());
  return newResponse;
}

Cache Invalidation

Vercel has on-demand revalidation. On Cloudflare:

KV-based Invalidation

// Invalidate specific key
await env.CACHE.delete(`post:${postId}`);

// Invalidate by prefix (requires listing)
const keys = await env.CACHE.list({ prefix: 'post:' });
await Promise.all(keys.keys.map(k => env.CACHE.delete(k.name)));

Cache API Purge

// Purge specific URL
const cache = caches.default;
await cache.delete(new Request('https://example.com/api/posts'));

// For broader purges, use Cloudflare API
await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiToken}` },
  body: JSON.stringify({ files: ['https://example.com/api/posts'] })
});

Practical Patterns

Stale-While-Revalidate

async function swr(key: string, fetcher: () => Promise<any>, ttl: number) {
  const kv = env.CACHE;
  const cached = await kv.get(key, 'json');

  if (cached) {
    if (Date.now() < cached.staleAt) {
      return cached.data; // Fresh
    }
    // Stale - return but refresh in background
    ctx.waitUntil(refresh(key, fetcher, ttl));
    return cached.data;
  }

  return refresh(key, fetcher, ttl);
}

async function refresh(key: string, fetcher: () => Promise<any>, ttl: number) {
  const data = await fetcher();
  await env.CACHE.put(key, JSON.stringify({
    data,
    staleAt: Date.now() + ttl * 1000
  }));
  return data;
}

Next: Migration Checklist - Step-by-step guide to moving from Vercel.

On this page