Over the last seven posts we’ve inched our way from the basics of static caching to the gnarly edges of full-page SSR caching. Each layer shaved milliseconds off the critical path, but there’s a ceiling you can’t break with origin-side tricks alone. That ceiling is physical distance. A user in Doha still waits on round-trips to Frankfurt or Virginia unless you give them a copy of your content closer to home.

That’s where a Content Delivery Network (CDN) becomes the force-multiplier in your Next.js performance playbook. By scattering cached responses across hundreds of edge POPs, a CDN turns latency from a continent-spanning problem into a cross-city hop—often cutting Time-to-First-Byte by 70-90 ms on typical ecommerce payloads.

But raw speed isn’t enough. Successful CDN caching means knowing what to cache, how long, and when to re-validate—especially with modern rendering patterns like ISR, streaming SSR, and Edge Functions that live somewhere between “static” and “dynamic.” Misconfigure a header and you’ll serve yesterday’s price to a million shoppers or blow your hit rate with an errant auth cookie.

In this chapter we’ll zoom out to the global layer and learn how to:

  • Map each Next.js rendering mode (SSG, ISR, SSR, Edge) to the right CDN policy.
  • Craft Cache-Control directives (s-maxage, stale-while-revalidate, immutable) that both browsers and CDNs respect.
  • Configure provider-specific rules on Vercel CDN, Cloudflare, and AWS CloudFront without locking yourself into a single host.
  • Purge and re-validate content automatically when content editors hit Publish—no 3 a.m. “cache-busting” deploys required.
  • Debug cache hits, misses, and unexplained STALE responses like a pro using headers, DevTools, and edge analytics.

By the end you’ll have a battle-tested checklist for global caching that keeps First-Time-Visitors wowed and returning users perpetually up-to-date—all while slashing your origin bill and keeping Lighthouse happy. Let’s bring your app closer to every user on the planet.

How CDNs Work with Next.js — A Pragmatic Primer

Before we dive into headers and provider dashboards, let’s ground ourselves in the mechanics of why a CDN can even cache your Next.js output.

The Life of a Request (30 000 ft View)

  1. DNS → Anycast POP

    A visitor in Singapore asks for www.yoursite.com. DNS hands back an IP that actually belongs to many edge servers. BGP routing steers the TCP connection to the nearest Point-of-Presence (POP).

  2. Edge TLS + Layer-7 Logic

    The CDN terminates TLS, strips off a few microseconds of handshake latency, and checks its local cache store (RAM / SSD).

  3. Cache Lookup

    HIT → Serve bytes immediately.

    MISS → Forward the request upstream (your Vercel deployment, a custom origin running Next.js, or another tier of CDN).

  4. Store & Serve

    When the origin responds, the edge stores the object keyed by URL plus whatever “vary” dimensions you’ve instructed (cookies, headers, query string). Subsequent users within the same geography now get sub-50 ms TTFB.

Rule of Thumb: An object lives in a POP until the least of (a) its s-maxage, (b) LRU eviction from limited space, or (c) an explicit purge.


What Next.js Produces (and Where It Lands)

Artifact Generated By Default CDN Behaviour (Vercel) Why It Matters
/_next/static/* (JS/CSS chunks) next build (immutable) Cached forever (immutable, max-age=31536000) Fingerprinted filenames guarantee uniqueness, so long TTL is safe.
Images (/_next/image or remote) next/image loader Cached per request params¹ Width/quality variants create unique URLs, perfect for edge caching.
HTML (SSG) next build Cached until next deploy Treated as static by default—one copy per locale/route.
HTML (ISR) App Router revalidate Cached for revalidate seconds After TTL, first request triggers background re-render.
HTML (SSR / Edge) Request-time rendering Not cached unless you add headers Gives you control to vary by cookie, auth, etc.

¹ Vercel CDN, Cloudflare Images, or CloudFront behave similarly because the loader appends width/quality params—effectively a fingerprint.


Where CDNs Slip Up with Frameworks

  • Header Blindness

    Many beginners rely on browser-focused max-age. CDNs actually obey s-maxage if it exists, otherwise they fall back to max-age. Forget the s- prefix and you’ll wonder why every edge server is a permanent cache-miss.

  • Cookie Pollution

    A single Set-Cookie: session=abc123 flag on your marketing page tells most CDNs “do not cache, this is personalized.” Solution: set Cache-Control: public and clear any personalization cookies server-side before the CDN sees them.

  • Hidden Query Strings

    CloudFront treats ?utm_source= as a new cache key by default if Query String Forwarding is enabled. Vercel ignores query strings unless you opt in via Route Segment Config. Know your provider’s defaults or watch your hit ratio tank.


Next.js 15 + CDNs in Practice

  • Vercel: Integrates its own edge network. Most of the heavy lifting (immutable asset headers, ISR revalidation) “just works” as long as you leave the defaults intact. You only intervene for SSR pages, auth-based variation, or experimental Edge Functions.
  • Self-Hosted (Cloudflare / CloudFront): You’re in charge of headers and origin shielding. A bad header combo on one route can blow away cache for your whole site. Treat every res.setHeader('Cache-Control', …) as a production-level contract.
  • Hybrid Origins: It’s perfectly valid to serve static assets from Vercel, dynamic API from AWS behind CloudFront, and images via Cloudflare—just keep the cache contract consistent so debugging stays sane.

Key takeaway: A CDN is only as smart as the hints you give it. Next.js produces the right artifacts out of the box, but telling the edge how long to keep each one—and when to ignore cookies, query strings, or headers—is the difference between a 90 % hit ratio and a sluggish worldwide experience.

Up next, we’ll map those rendering modes to concrete caching strategies—and show exactly which Cache-Control spell to cast for each.

Caching Strategy per Rendering Method

Not every page deserves the same shelf life—nor the same place on that shelf. A marketing homepage can chill in the edge cache for a year, while a stock-price widget should barely unpack its bags. The trick is pairing each Next.js rendering mode with a matching CDN recipe so you keep hit ratios high and data fresh.

Rendering Mode Typical TTL Recommended Cache-Control header Rationale
SSG (Static Site Generation) Months / until next deploy public, max-age=31536000, immutable Fingerprinted assets & prebuilt HTML never change; browsers and CDNs can keep them forever.
ISR (Incremental Static Regeneration) Seconds → minutes public, s-maxage=60, stale-while-revalidate=300 First hit after TTL sees cached page; edge quietly re-renders in the background.
SSR (Server-Side Rendering) Seconds public, s-maxage=30, must-revalidate Good for anonymous traffic where data changes often but not that often.
Edge/Streaming Per request Usually uncached or keyed on cookie/header Personalization or low-latency computations—better to skip CDN storage unless variant keys are tight.

Tip: CDNs ignore max-age in the presence of s-maxage, so always set the latter for edge-side decisions and let browsers fall back to the former.

How a Request Flows Through the Layers

graph LR
A[Browser] -->|GET /product/slug| B[CDN POP]
B -->|HIT| C[Serve Cached HTML]
B -->|MISS| D[Next.js Origin]
D --> E[Generate HTML]
E -->|Store & Stream| B
  • A HIT terminates at B—the edge node responds in ±20 ms.
  • A MISS walks down to your origin, but the freshly rendered HTML is pushed right back to the edge so the next visitor enjoys the shortcut.

Matching Strategy to Business Requirements

Scenario Best Fit Why
Product catalog that updates hourly ISR Fast reads, predictable refresh window.
Flash-sale countdown page SSR / Edge Needs real-time stock & pricing, can’t risk staleness.
Docs site or blog SSG Content seldom changes; max cache benefit.

Sample Header Snippets

// SSG API Route (headers set at build time)
export const revalidate = false; // Static forever

// ISR Page component
export const revalidate = 60; // in seconds
export async function generateStaticParams() {/* ... */}

// SSR Route Handler
export async function GET() {
  const res = await fetchProducts()
  return NextResponse.json(res, {
    headers: {
      'Cache-Control': 'public, s-maxage=30, must-revalidate',
    },
  })
}

Avoiding Variant Explosion

  • Cookies: Strip or prefix them (Cache-Control: public) if the data is the same for guests.
  • Query Strings: In CloudFront, disable “Forward query strings” unless absolutely required.
  • Accept-Language / Geo: Use Edge Middleware to funnel just the needed header values into the cache key instead of the entire header.

Dial these levers correctly and you’ll watch your edge hit ratio climb while your origin CPU naps. In the next section we’ll roll up our sleeves and write the exact Cache-Control spells each CDN understands—plus the edge-specific quirks that can make or break those dreams of 95 % HITs worldwide.

Crafting Cache-Control Headers That CDNs Actually Obey

Setting a header is easy; choosing the right header that browsers and every POP between São Paulo and Seoul respect is the art form. Think of Cache-Control as the recipe card you hand to the CDN: how long to keep the dish on the counter, when to toss it, and whether guests (browsers) may take leftovers home.

Decode the Cache-Control Lexicon

Directive Who Reads It What It Means in Practice
public Browsers, CDNs Response is safe to store even if it contains cookies.
max-age= Browsers How long (in seconds) the browser can reuse the asset without re-checking the origin.
s-maxage= CDNs only Overrides max-age for shared caches. Your edge nodes live by this value.
stale-while-revalidate= Browsers, CDNs* Serve the stale copy immediately, but fire a background re-fetch to refresh the cache.
immutable Browsers “Don’t bother re-validating—this file is fingerprinted.” Perfect for /static/chunk-abc123.js.
must-revalidate Browsers, CDNs Once the TTL hits zero, fetch a fresh copy before replying. Good for sensitive price data.

Not every CDN respects stale-while-revalidate yet. Vercel and Cloudflare do, CloudFront does not unless you run a Lambda@Edge shim.

Header Recipes for Common Next.js Pages

Static asset (JS, CSS, fonts)

(You get this free from next build, but it’s handy to know what it means.)

Cache-Control: public, max-age=31536000, immutable

ISR page that rebuilds every minute, but should stay lightning-fast for users

Cache-Control: public, s-maxage=60, stale-while-revalidate=300

Edge rule of thumb: stale-while-revalidate ≥ 5 × s-maxage keeps hit rates high while giving you a generous freshness window.

Anonymous SSR page that must never be older than 30 seconds

Cache-Control: public, s-maxage=30, must-revalidate

Personalized SSR page (skip caching entirely)

Cache-Control: private, no-store

By marking it private, you instruct the CDN to bypass storage while still letting the browser cache it if you wish (rare for auth pages).

Implementing Headers in Next.js 15

App Router makes header setting almost trivial:

// app/(public)/blog/[slug]/page.tsx
import { headers } from 'next/headers';

export const revalidate = 60; // ISR, but we'll tune the edge TTL

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  // Edge TTL: 60 s, but allow 5 min stale serving
  headers().set(
    'Cache-Control',
    'public, s-maxage=60, stale-while-revalidate=300'
  );

  return <Article data={post} />;
}

For Route Handlers or traditional API routes:

export async function GET() {
  const data = await expensiveQuery();
  return NextResponse.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=10, stale-while-revalidate=30',
    },
  });
}

Provider-Specific Gotchas

  • Vercel CDN
    • Reads s-maxage religiously.
    • Exposes the verdict via x-vercel-cache: HIT | MISS | STALE.
    • Omits stale-while-revalidate if you accidentally append private.
  • Cloudflare
    • Treats max-age and s-maxage the same unless you enable Cache Everything or write a Worker.
    • Cookies bust the cache unless you declare cacheEverything() in Workers or strip cookies upstream.
  • AWS CloudFront
    • Ignores stale-while-revalidate.
    • Caches by default on origin headers only; if you vary on Accept-Language, enable Header Whitelisting or Lambda@Edge.

Quick Sanity Checklist

  1. Start with public unless the page is truly user-specific.
  2. Always provide s-maxage—otherwise the CDN falls back to the more conservative max-age.
  3. Keep stale-while-revalidate ≥ 5× s-maxage for high-traffic ISR routes.
  4. Strip or hash volatile cookies in Edge Middleware so they don’t kill your cache.
  5. Watch your headers in DevTools → Network → Response; verify Age ticks up on refresh. No increase? You’re missing the cache.

Dial these headers in, and global POPs turn into mini-origins that serve your pages with coffee-shop proximity. Next we’ll leave theory behind and jump into the provider consoles—Vercel, Cloudflare, and CloudFront—to wire up the exact rules that make those headers come alive.

Configuring CDN Rules on Vercel, Cloudflare and AWS CloudFront

You’ve written the perfect Cache-Control spells—now the edge needs to obey them unfailingly. Each CDN has its own levers, defaults, and gotchas. Get familiar with the dashboard quirks once and your hit-ratio graphs will stay green for life.


Vercel CDN – Native Integration Done Right

Vercel’s edge network is purpose-built for Next.js, so a lot “just works,” but there’s still room for fine-tuning.

What You Want Where to Do It Pro Tip
Override caching per route Add export const revalidate = … or headers().set() in the App Router. Keep the project-level Performance → Caching setting on Automatic unless you’re debugging.
Cache variants by cookie or header middleware.tsconst res = NextResponse.next({ request: { headers: { 'x-geo': geo } } }) Any header you append in Middleware becomes part of the cache key—cheap way to localize pages.
Debug live traffic x-vercel-cache header HIT (served from edge), STALE (edge used SWR), MISS (origin). Aim for ≥ 90 % HIT/STALE.
Purge instantly Vercel Dashboard → DeploymentsInvalidate Cache Works per deployment; no wildcard path purges needed.
// Example: Add locale to cache key without bloating cookies
export function middleware(req: NextRequest) {
  const country = req.geo?.country || 'default';
  const res = NextResponse.next();
  res.headers.set('x-country', country);
  return res;
}

Cloudflare CDN – Power Tools for the Tinkerer

Cloudflare won’t cache HTML unless you ask politely. Happily, you have three ways to ask.

1 Page Rules (the fastest on-ramp)

  1. Navigate to Rules → Page Rules → Create Rule.
  2. Pattern: example.com/blog/*
  3. Set Cache Level → Cache Everything and Edge Cache TTL → 1 hour.

Great for blogs or marketing paths that never set auth cookies.

2 Transform Rules or Workers (full control)

export default {
  async fetch(req, env, ctx) {
    // Strip cookies to keep variant count down
    const url = new URL(req.url);
    if (url.pathname.startsWith('/api')) return fetch(req); // bypass

    const newReq = new Request(req, { headers: new Headers(req.headers) });
    newReq.headers.delete('cookie');

    const res = await fetch(newReq);
    res.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
    return res;
  },
};

Deploy as a Cloudflare Worker → “Routes” → match `*example.com/`.*

Workers respect stale-while-revalidate, giving you near-instant refresh without cold misses.

3 R2 + KV for Dynamic HTML

For heavy traffic landing pages, render once in Next.js, store in KV, and serve directly from Workers—bypassing your origin entirely after the first request.


AWS CloudFront – Enterprise-Grade, More Knobs

CloudFront is rock-solid, but it expects explicit instructions.

Essential Behavior Settings

Setting Recommended Value
Origin Cache Policy CachingOptimized (static) or custom policy for HTML
Header Forwarding Whitelist only those you vary on (Accept-Language, custom AB header).
Query Strings None unless product filters live in the query.
Cookies Forward = None for public pages; Whitelist when needed.
Minimum TTL 0 (sec) so s-maxage always wins.

Lambda@Edge for SWR-Like Behavior

CloudFront ignores stale-while-revalidate, but you can emulate it:

// viewer-response.js
'use strict';
exports.handler = async (event) => {
  const res = event.Records[0].cf.response;
  const age = parseInt(res.headers.age?.[0]?.value || '0', 10);

  // If object is older than 60 s, trigger async refresh
  if (age > 60) {
    const url = 'https://' + res.headers['x-amz-meta-refresh-origin'][0].value;
    fetch(url, { method: 'PURGE' }).catch(() => {});
  }
  return res;
};

Attach this at the Viewer-Response stage and keep your TTL short (e.g., 60 s). The first viewer after expiry still gets the stale copy, but Lambda fires off the background refresh.

Cache Invalidation

aws cloudfront create-invalidation \
  --distribution-id E123ABC456 \
  --paths "/blog/*" "/products/*"

Trigger this script from your CMS webhook so editors see updates in seconds.


Universal Debugging Tricks

  • curl -I https://site.com — Check Cache-Control, age, x-cache or x-vercel-cache.
  • Chrome DevTools → Network → Timing — Look for a sub-100 ms Waiting (TTFB) on a warm cache.
  • Provider Analytics — Vercel Edge Network, Cloudflare Cache Analytics, CloudFront CloudWatch hit/miss graphs.

If numbers sag, inspect which header, cookie, or query param is secretly exploding your cache key space.


Your CDN is now primed, tuned, and ready to throw pages around the planet at the speed of light—well, as close as the laws of physics allow. Next up we’ll explore stale-while-revalidate in depth and show how to deliver instant responses and background freshness without lifting a finger.

Harnessing stale-while-revalidate for Instant Speed & Silent Freshness

Imagine serving every page in ±30 ms and letting the CDN refresh content while your visitor is already scrolling. That’s the promise of stale-while-revalidate (SWR) at the HTTP‐header level—not to be confused with the React data-fetching hook of the same name.

What SWR Really Does

  1. During the TTL (s-maxage): the edge returns a pure HIT.
  2. After TTL but within SWR window:
    • The CDN serves the stale copy immediately—still a sub-50 ms response.
    • In the background it fetches a fresh version from your origin and replaces the cached object.
  3. After both TTL & SWR expire: the next request blocks until the origin responds (behaves like a traditional MISS).

Mental model: You trade eventual freshness for immediate speed, but only for a tightly bounded time window you control.

Choosing the Right Durations

Page Type s-maxage stale-while-revalidate Why
News home page 60 s 300 s Visitors get near-real-time headlines; editors see updates within 5 min.
Product catalog 120 s 600 s Prices rarely change more often than every 10 min; cache hit rate stays high.
Live sports scores 5 s 20 s Ultra-tight TTL but still cushions the origin during viral spikes.

Rule of thumb: make the SWR window 4–10 × the s-maxage value so most requests remain full-speed hits while the edge quietly refreshes.

End-to-End Example in Next.js

// app/products/page.tsx
import { headers } from 'next/headers';

export const revalidate = 120; // ISR fallback for safety

export default async function Products() {
  const products = await getVisibleProducts();

  // Tell the CDN: use for 2 min, serve stale for up to 10 min
  headers().set(
    'Cache-Control',
    'public, s-maxage=120, stale-while-revalidate=600'
  );

  return <Catalog items={products} />;
}

Flow in real life

sequenceDiagram
  participant User
  participant Edge
  participant Origin

  User->>Edge: Request /products
  Edge-->>User: Cached HTML (age 0-120s)
  Note over Edge: 0-120 s: Perfect HIT

  User2->>Edge: Request (age 180 s)
  Edge-->>User2: Stale HTML (instant)
  Edge->>Origin: Async re-fetch
  Origin-->>Edge: Fresh HTML
  Note over Edge: Cache updated, no user waits

  User3->>Edge: Request (age 601 s)
  Edge->>Origin: Wait for HTML
  Origin-->>Edge: New HTML
  Edge-->>User3: Fresh HTML

Provider Realities

Provider SWR Support Quirk to Watch
Vercel CDN ✅ Native x-vercel-cache: STALE flags SWR hits for debugging.
Cloudflare ✅ Works with Cache Everything or Workers If using Workers, set cf.cacheTtl() and header for consistency.
AWS CloudFront 🚫 No native SWR Emulate with Lambda@Edge: serve stale, trigger async refresh via background PURGE.

Debugging SWR in Production

  • Look for “STALE” headers

    Vercel: x-vercel-cache: STALE

    Cloudflare: cf-cache-status: REVALIDATED

  • Watch the Age value

    Reload twice—first time you may see STALE with age > s-maxage; second time should drop to near-zero after refresh.

  • Monitor origin QPS

    A healthy SWR setup keeps origin requests flat during traffic spikes—graphs should stay calm even when your marketing campaign goes viral.

When Not to Use SWR

  • Highly volatile financial data (e.g., crypto tickers).
  • User-specific dashboards where personalization outweighs speed.
  • Security-sensitive pages (OTP, checkout steps).

Everywhere else, stale-while-revalidate is your secret weapon: users get blisteringly fast first paints, your servers take a nap, and the content never lingers long enough to feel outdated.

With instant-plus-freshness now under your belt, it’s time to tackle the final puzzle piece: keeping those caches in sync through smart, automated invalidation—and knowing exactly which header or cookie sabotages the party when things go sideways.

Smart Cache Invalidation Techniques

Caching is only half the equation—knowing exactly when to kick stale content to the curb is what keeps users (and stakeholders) happy. The goal is precision: purge only what changed, as soon as it changes, without vaporising your hard-won 95 % hit ratio.

Why Invalidation Matters

  • Dynamic businesses — today’s hero banner becomes tomorrow’s clearance sale.
  • SEO freshness signals — Google rewards pages that show updated offers in structured data.
  • Analytics accuracy — a cached page may still load a new analytics script, but if the HTML shell is two deployments behind, you’re measuring yesterday’s funnel.

Three Axes of Invalidation

Trigger Type Typical Tooling When to Use
Time-based (TTL) s-maxage / Redis EX Predictable churn: daily pricing updates, hourly dashboards.
Content-driven Webhooks, Vercel revalidate* APIs, CMS events Editors need “publish → live in 10 s” feedback loops.
Manual / Emergency Dashboard “Purge” buttons, CLI scripts Legal takedowns, broken deploys, or hot-fix rollbacks.

Leveraging Next.js Revalidation APIs

Next.js 15 ships first-class helpers that integrate directly with Vercel’s tag-based cache purge system:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(req: Request) {
  const { tag } = await req.json();      // e.g. "blog-post-42"
  await revalidateTag(tag);
  return Response.json({ revalidated: true });
}

In your CMS webhook → POST { "tag": "blog-post-42" } and the edge instantly evicts that single item—no global purge, no spectator downtime.

Other helpers:

await revalidatePath('/blog');        // nuke one route
await revalidateTag('product');       // nuke everything tagged “product”

Pro tip: tag pages by content type (product, blog) and individual IDs (product-123). Editors usually hit the broad tag; engineering can surgically evict a single SKU if price data looks off.


Cloudflare API Purges

For origin-hosted sites on Cloudflare, wire a webhook that rides the Zone Purge by URL endpoint:

await fetch(
  `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${CF_API_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ files: ['https://example.com/blog/post-42'] }),
  }
);

Skip wildcards unless absolutely necessary. Purging /blog/* wipes the cache for all posts, while most editors only changed one.


CloudFront Invalidation in CI/CD

Attach a simple shell step at the end of your GitHub Actions deploy:

aws cloudfront create-invalidation \
  --distribution-id $CF_DISTRIBUTION \
  --paths "/blog/post-42" "/_next/data/*/post-42.json"
  • Invalidate HTML plus the associated JSON route data (/_next/data/...).
  • Keep the list short—CloudFront charges after 1 000 paths per month.

Surrogate-Key Patterns for Headless CMSes

If you control the edge (Fastly, Cloudflare Workers), add a Surrogate-Key header:

Surrogate-Key: product product-123 category-shoes

Then purge by key:

curl -X PURGE -H "Fastly-Key: $TOKEN" \
     -H "Fastly-Soft-Purge: 1" \
     "https://api.fastly.com/service/$SID/purge/product-123"

Soft-purge means the old object serves as STALE until the new one is fetched—zero downtime, zero cold starts.


Putting It All Together

  1. Define cache tags when you model your content: posts, products, categories.
  2. Emit those tags as headers (x-cache-tag) or via Next.js revalidateTag() calls.
  3. Wire CMS webhooks to hit your invalidation endpoint the moment an editor clicks Publish.
  4. Fall back to TTL for data that changes on a schedule (currency rates, weather).
  5. Keep emergency paths handy—CLI scripts, dashboard buttons—for the inevitable “we need that gone now” call at 1 a.m.

Get these guardrails in place and you’ll wield caching like a lightsaber: clean cuts, zero collateral damage.

Next we’ll slide under the hood with debugging tactics—reading headers, tracing cache keys, and spotting silent cookie bombs—so you can fix cache misses before the customer even notices.

Debugging CDN Cache Mysteries Like a Detective

Even with picture-perfect headers, gremlins lurk—rogue cookies, sneaky query strings, or a POP that simply decided to ignore you. Here’s a battle-tested workflow to diagnose “Why is this page still slow in Sydney?” before it hits the CEO’s inbox.


Start With the Headers

curl -I https://example.com/product/air-max
Header Healthy Value Red Flag
cache-control public, s-maxage=60, stale-while-revalidate=300 private, missing s-maxage, or a TTL of 0.
age Growing on repeat requests (e.g., Age: 42) Always 0 → POP never caches.
CDN verdict (x-vercel-cache, cf-cache-status, x-cache) HIT or STALE MISS, BYPASS, ERROR
set-cookie None (or stripped) Personalized cookies on a public page—kills cache.

Tip: Run the curl command twice in a row. The second response should show a higher Age if caching works.


Chrome DevTools Deep Dive

  1. Open Network tab → disable cache (for a clean slate).
  2. Load the page once, then re-enable cache.
  3. Right-click → Clear Browser Cache. Reload again.
  4. Watch TimingWaiting (TTFB).
    • <100 ms from any region: edge HIT.
    • 300–800 ms: origin round-trip. Check your POP map.
  5. Under Headers, confirm cf-cache-status, x-vercel-cache, or x-cache.

Spot the Usual Suspects

Symptom Likely Culprit Fix
TTFB fast for /, slow for /?utm=… Query string variation Disable query forwarding in CloudFront, or ignore analytics params via Edge Middleware.
Misses only for logged-in users Auth cookies cut across the cache key Route logged-in paths to SSR; strip cookies on public ones.
Random BYPASS at one POP Over-quota or evicted cache Raise plan limits, or lower object size (≥2 MB objects get purged first).
High Age, but data still old Incorrect revalidation logic Verify webhook fires, confirm revalidateTag() endpoints return 200.

Provider-Specific Toolkits

Provider Built-In Console What It Shows
Vercel Analytics → Edge Network Real-time HIT / MISS / STALE ratio per region; drill into individual POPs to spot outliers.
Cloudflare Cache Analytics Heatmap of top URLs by MISS count, ratio over time, and reason (cookie, TTL).
CloudFront CloudWatch Metrics Misses, HitRate, plus Lambda@Edge logs if you’re simulating SWR.

Trace a Single Request End-to-End

  1. Generate a unique header on your local machine:

    TOKEN=$(openssl rand -hex 4)
    curl -I -H "x-debug-token: $TOKEN" https://example.com/page
    
  2. Log that header at your origin (Next.js Request Headers log) and at any middleware/workers.

  3. Compare timestamps

    Edge log vs origin log latency reveals exactly where the request detoured.


When All Else Fails

  • Purge single URL and retest—if the issue vanishes, suspect expired variants or header drift.
  • Check POP coverage—some providers exclude small regions on free tiers; your Sydney friend may be hitting Singapore.
  • Shoot the cookie messenger—add this quick snippet in middleware.ts to wipe analytics cookies for public routes:

    export function middleware(req: NextRequest) {
      const res = NextResponse.next();
      if (!req.nextUrl.pathname.startsWith('/admin')) {
        res.headers.set('set-cookie', '');
      }
      return res;
    }
    

Master these sleuthing techniques and you’ll turn every “site feels slow here” complaint into a five-minute fix—long before the ticket escalates. With debugging in your toolbelt, you’re ready to wrap up with the golden rules that keep a Next.js CDN setup humming year-round.

Best Practices for a CDN-Optimized Next.js Setup

Theme Guideline Why It Matters
Cache boldly, invalidate precisely Treat every byte that leaves your origin as guilty until cached. Pair long-lived objects with webhook or tag-based purges instead of shortening TTLs “just in case.” High hit ratios slash global latency and infrastructure cost.
Keep the cache-key diet clean Vary only on headers you actually need (Accept-Language, x-ab-test). Strip analytics query strings and user cookies in middleware. Reduces key explosion; one object per POP instead of thousands.
Combine ISR + SWR for perfect UX Let ISR handle origin re-generation while SWR gives the edge freedom to serve stale copies during the rebuild window. Users see fresh-ish content instantly; editors see updates in seconds.
Monitor, don’t assume Track HIT/STALE/MISS ratios, TTFB, and origin QPS in dashboards. Set alerts when hit ratio dips below target. Caching bugs surface as $$ bills or SEO penalties—catch them early.
Automate purges in CI/CD Post-deploy hook → purge changed routes; CMS webhook → revalidateTag; Git commit message → map to Surrogate-Key. No human remembers every dependency tree; scripts do.
Secure the edge Cache public pages, but route anything with PII or payments through SSR with private, no-store. Audit headers to prevent leaky auth tokens. Performance never trumps privacy or compliance.
Test across geographies Load-test from at least three continents. Verify POP coverage and fail-over behaviour if a region goes dark. A cache hit in Frankfurt means nothing if Sydney always cold-starts.
Version your cache rules Keep cache-rules.ts in repo. Review diff like code. Roll back with git tags if a header change tanks SEO. Treat caching logic as first-class application code.

Conclusion

A smart CDN configuration turns your Next.js site into a world-wide neighborhood shop: pages pop open in a heartbeat for users in Doha, Dallas, or Düsseldorf, your servers stay cool, and your DevOps team finally sleeps through traffic spikes.

Master the trio—headers, provider rules, and surgical invalidation—and you’ll deliver near-static speed for dynamic experiences without risking stale prices, out-of-date promos, or SEO drags. From here on out, distance is just another layer you already conquered.


Coming Up Next

The next post dives deep into image optimisation and caching with next/image, custom loaders, and CDN transforms—because nothing ruins a lightning-fast HTML delivery like a 4 MB hero shot.


Related Links

This article is part of the “Caching in Next.js – A Comprehensive Blog Series.” Stay tuned for more, and feel free to explore the earlier chapters to build the full performance toolkit.