Browser Cache vs CDN Cache vs Next.js Cache, Explained
Tutorial

Browser Cache vs CDN Cache vs Next.js Cache, Explained

A deploy goes out, the origin is correct, and a user still sees the old page for hours. That's not a bug in any one layer — it's three separate caches, each keyed differently, each invalidated by something else entirely.

Published September 21, 202611 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

Why does a page that's already updated on the server still look stale to a real visitor, sometimes for hours?

Three separate caches sit between a change and a visitor's screen — the browser's own cache, a CDN edge cache, and Next.js's own server-side cache — and each one keys on different things and gets cleared by different triggers. A browser respects the Cache-Control header it was sent and won't ask again until that says to. A CDN edge respects the same header but adds its own rules and TTL floors on top. Next.js's cache is a third, separate thing again — a disk-backed store keyed by route and revalidation tag that the other two layers never see directly. "It's still slow even behind a CDN" is almost always one of these three not doing what someone assumed, not a CDN failing at its job.

  • The browser cache, CDN cache, and Next.js server cache are three different systems, not three names for the same idea
  • Immutable assets get public, max-age=31536000, immutable; ISR pages get s-maxage=<revalidate>, stale-while-revalidate; dynamic pages get private, no-cache, no-store, max-age=0, must-revalidate
  • A CDN only caches what its Cache-Control header tells it to — a dynamic route marked private never gets cached there no matter how good the CDN is
  • Cloudflare's free plan can't set an edge cache TTL below 2 hours via rules, which is exactly the kind of gap that makes a fresh deploy look stale to real visitors
  • revalidateTag and revalidatePath only clear Next.js's own cache on the instance that ran them — a CDN edge cache and every visitor's browser cache are untouched by that call

Evidence notes

Cache-Control values by response type

Next.js sets public, max-age=31536000, immutable on truly immutable hashed assets; s-maxage=<revalidate>, stale-while-revalidate on ISR pages; and private, no-cache, no-store, max-age=0, must-revalidate on dynamically rendered pages.

Next.js's server cache is disk-backed and per-instance by default

Caching and ISR share one server cache stored on local disk per instance by default; multiple instances need a custom cacheHandler with cacheMaxMemorySize set to 0 to stay consistent, and the in-memory default caps at 50 MB.

Cloudflare free plan cache TTL floor

Free zones cannot set an edge cache TTL below 2 hours via cache rules, checked against Cloudflare's own documentation in September 2026.

Continue with purpose

The deploy finished. The database is updated. The origin server, if you curl it directly, returns the new content. And a real visitor, in a real browser, still sees the old page — sometimes for minutes, sometimes for most of a day. Nobody broke anything. Three separate caches sit between that origin and that visitor, and each one is doing exactly what it was told, which is the actual problem: nobody told all three of them the same thing.

What are the three layers, actually?

Layer one is the browser's own HTTP cache, sitting on the visitor's device. It reads the Cache-Control header on a response and, if that header says a resource is good for an hour, simply doesn't ask the server again for an hour — not because the server is slow to respond, but because the browser was told not to bother asking. Layer two is a CDN edge cache — Cloudflare, in most self-hosted setups — sitting between the origin and every visitor, caching a copy of a response so the next thousand visitors from the same region hit the edge instead of the actual VPS. Layer three is Next.js's own server-side cache: ISR output and cached data, stored on the origin itself, keyed by route and by cache tag, and invisible to the other two layers entirely. I write about browser cache vs CDN cache vs next.js cache, explained from the build side; XenGrowth's marketing operations practice covers what it takes to run it.

These aren't three names for the same mechanism wearing different hats. They're three separate stores, controlled by three separate things, and a change that clears one of them does nothing to the other two unless someone explicitly wires that up.

Layer

Where it lives

What clears it

Browser cache

The visitor's own device

The Cache-Control max-age expiring, or a hard refresh — nothing the server does after the fact reaches it

CDN edge cache

Points of presence between the origin and every visitor

A cache purge call to the CDN's API, or the edge TTL expiring on its own

Next.js server cache

Local disk (or a custom cache handler) on the origin itself

revalidateTag / revalidatePath, or the ISR revalidate window elapsing

What Cache-Control value does each kind of page actually get?

This is the part worth memorizing rather than guessing at, because the header is the only thing the browser and the CDN ever actually see — neither of them knows anything about ISR or revalidate windows except through this one line.

Response type

Cache-Control header

Browser behavior

CDN behavior

Immutable static assets (hashed filenames — JS, CSS, images from static imports)

public, max-age=31536000, immutable

Cached for a year, never re-requested, never revalidated

Cached indefinitely at the edge — safe because the filename itself changes on any real change

ISR / revalidated pages

s-maxage=<revalidate>, stale-while-revalidate

Respects max-age if present, otherwise treats it like a normal response

Serves the cached copy instantly while revalidating in the background, if the CDN respects stale-while-revalidate

Dynamically rendered pages

private, no-cache, no-store, max-age=0, must-revalidate

Never cached — every request re-validates or re-fetches

Never cached — a CDN has no legal basis to cache a private response, full stop

That third row is where "it's still slow even with a CDN in front" usually comes from. A CDN can't cache a response marked private, no-cache, no-store — that header exists specifically to say don't cache this, and every well-behaved CDN honors it. If a page is rendering dynamically because it reads cookies, headers, or search params, it's getting that header whether or not a CDN sits in front of it, and the CDN is doing precisely nothing for that route. That's not a CDN failure. That's the CDN correctly refusing to cache something it was told not to. If the operations side of this is the part you are stuck on, The XenGrowth resource library is the better reference.

So why does a deploy stay stale for hours specifically?

Because the CDN edge cache and the browser cache both operate on their own clocks, independent of whatever just happened at the origin. If an ISR page was cached at the edge with a stale-while-revalidate window, and the CDN's own minimum cache floor is higher than the page's own revalidate time, the CDN's floor wins — Cloudflare's free plan, for instance, can't set an edge cache TTL below two hours through cache rules, no matter what the origin's own header says the page's revalidate interval should be. A page configured to revalidate every five minutes at the Next.js layer can still sit unchanged at the edge for two hours, because the CDN's own rule floor is the thing actually governing that copy, not the origin's more generous intention.

  • revalidateTag() or revalidatePath() runs on the origin and clears Next.js's own on-disk cache — it does not touch a CDN edge cache or any visitor's browser cache

  • A CDN purge clears the edge cache — it does not touch Next.js's own server cache, which will just regenerate the same content it already had if nothing else changed

  • A hard refresh clears one visitor's browser cache — it touches nothing upstream of that one browser

  • None of these three actions is a substitute for either of the other two; a real "push this change live everywhere immediately" flow has to trigger all three deliberately

What about multiple app instances — does that change anything?

Yes, and it's a separate problem from the CDN layer entirely. Next.js's caching and ISR share one server cache, stored on local disk per instance by default — fine for a single self-hosted instance, but each additional instance keeps its own separate copy of that cache unless a custom cache handler is configured to share it. Calling revalidateTag on instance A clears instance A's cache; instance B, sitting behind the same load balancer, keeps serving its own stale copy until it independently notices the change or its own revalidate window elapses. The fix Next.js documents is a custom cacheHandler backed by shared storage — Redis is the common example — with cacheMaxMemorySize set to 0 so the in-memory default (which caps at 50 MB per instance anyway) isn't fighting the shared store. This is exactly the same shape of problem as the CDN one — one clock per layer, and every layer needs to be told separately.

A worked example: a page that revalidates every five minutes

Say a product page is set to revalidate every 300 seconds. Next.js sends s-maxage=300, stale-while-revalidate. At the origin, that's exactly what happens: the page regenerates in the background roughly every five minutes, and Next.js's own on-disk cache holds whatever the latest generated copy is. So far, so simple — one layer, one clock, behaving exactly as configured. Now put a CDN with a two-hour minimum cache floor in front of it. The CDN caches its own copy of that response the first time it's requested, and holds onto it for its own two-hour minimum regardless of what the origin's s-maxage said it would prefer. For nearly two hours, every visitor hitting that CDN region gets the same cached copy, even though the origin has quietly regenerated that page twenty-some times in the background during the same window. The origin is doing exactly what it was told. The CDN is doing exactly what it was told. Neither one is broken, and the page is still stale for two hours from a visitor's point of view. XenGrowth on governed AI marketing workflows covers the AI agents and marketing automation side of this.

This is the scenario that produces the most confused bug reports in any self-hosted setup, because every individual piece of evidence looks correct in isolation. Curling the origin shows the new data. Checking the Next.js logs shows ISR regenerating on schedule. And a real visitor, five minutes after a price change, still sees the old price — not because anything crashed, but because the CDN's own rules are a second, independent cache with its own independent clock, sitting on top of a Next.js cache that was never in the loop on the CDN's decision to begin with.

Does a managed platform actually avoid this, or just hide it better?

Mostly the latter. A platform that owns both the origin and the CDN edge can wire revalidateTag to also purge the specific edge cache entry for that tag, in one call, because both systems are theirs to coordinate. That's a real advantage, and it's the thing self-hosted setups have to build deliberately instead of getting for free. It isn't a fundamentally different caching model, though — it's the same three-layer structure, with one company operating two of the three layers and making sure they talk to each other. A self-hosted stack with Next.js on a VPS and Cloudflare in front is running the identical architecture; it just has to call Cloudflare's purge API explicitly as part of a deploy or a content update, instead of that call being bundled invisibly into a platform's own revalidation path.

That distinction matters because it changes what "fixing" this actually means. It's not that self-hosting is missing a feature Next.js only unlocks on a managed platform — the three layers, the headers, and the invalidation rules are identical either way, right down to the exact Cache-Control strings. What's missing on a self-hosted stack is the glue code connecting them, and that glue code is something any team can write, because none of the three layers is doing anything proprietary. The gap is real, but it's a gap in wiring, not in capability. On AI search, GEO and discovery specifically, XenGrowth on building one SEO and GEO content system is worth reading.

  • A deploy hook that also calls the CDN's purge endpoint for changed paths closes most of this gap without waiting out the TTL floor

  • Cache tags at the CDN layer (where the CDN supports them) let a purge be scoped to exactly what changed, instead of purging the whole zone for one content update

  • For content that changes on a predictable schedule, setting the CDN's own cache rule TTL to match the ISR revalidate window — where the plan allows going that low — keeps the two clocks from fighting each other

  • For anything that must be visible everywhere the instant it changes, the honest fix is marking that specific route dynamic rather than fighting three layers of cache to make it look instant

A stale page after a correct deploy isn't proof something's broken. It's proof three clocks were never actually synchronized in the first place.

How do you actually debug "it's stale for some people but not others"?

Start from the origin and work outward, because the layers only ever get more stale as a request moves further from the server, never less. Curl the origin directly — if that's already wrong, none of the caching layers are the problem, the render itself is. If the origin's correct, check what Cache-Control header the response actually carries — a dynamic page marked no-store shouldn't be cached anywhere, so if it's stale, something else changed (a database read that isn't actually fresh, or a request hitting a different instance with its own uncoordinated cache). If the header says it should be cacheable and it's stale at the edge, that's a CDN purge that didn't happen, or a TTL floor like Cloudflare's two-hour minimum still running out its clock. If it's fine at the edge and stale for one specific visitor, that's their browser holding onto a cached copy under the max-age it was given, and nothing server-side is going to reach in and clear it for them.

The instinct to blame the CDN first is understandable — it's the layer people know exists and don't fully understand — but it's usually the least likely culprit of the three, precisely because it's the one most rigidly following the rules it was given. The header is either telling it to cache or telling it not to; there's no third option where a CDN just decides to be slow for no reason.

Further reading from XenGrowth

Where this work meets go-to-market

Working on browser cache vs CDN cache vs next.js cache, explained inside a commercial team? XenGrowth's growth operations team publishes operator guides on the revenue side of this work.

Three clocks, one deploy

Five questions on which cache actually clears when you think you've invalidated everything. Answers and reasoning at the end.

1 / 5
You call revalidateTag() right after a content update. What actually gets cleared?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

CachingCDNNext.jsCache-ControlISRPerformanceHTTPtutorials

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

What Cloudflare's Cache Actually Does for Speed and Bandwidth

Cache HIT and MISS aren't a minor speed difference — they're two different request paths entirely, one of which never reaches your server. Here's the mechanism, the free tier's real limits, and exactly how to check your own hit ratio instead of trusting a number nobody measured.

Navigate

How to Self-Host Next.js With Coolify: A Complete 2026 Walkthrough

Every Coolify tutorial stops at "push to deploy." This one covers the parts that actually break a real app: standalone output, a build that OOMs on a small box, env vars baked in at the wrong time, and a health check that would have caught all of it.

Navigate

Why Next.js Feels Slow on a VPS — and the Six Things That Fix It

The app that felt instant on Vercel goes home to a cheap VPS and forgets how to load fast. The framework didn't change. Six specific things underneath it did, and none of them are next/image.

Navigate

Core Web Vitals and SEO: What Changed Recently, and What Didn't

Core Web Vitals still matter for SEO, but INP replaced FID and Google's ranking weight shifted. What actually changed, what stayed the same, and what you should optimize for now.

Navigate

Where console.log Actually Goes When You Self-Host Next.js

On Vercel, a console.log just shows up in a dashboard somewhere. Self-hosted, it goes through a chain most people never trace end to end — and the two most common questions I get are why a log doesn't show up at all, and why the disk filled with logs nobody remembers writing.

Navigate

Designing Next.js Platforms That Stay Fast as Content Grows

Performance problems in large content platforms are almost never one slow query — they're architectural. Here's what actually keeps a Next.js platform fast as pages and teams both grow.

Navigate

How Far a €5.50 VPS Actually Scales: What the Numbers Say

Nobody's benchmark is your app's benchmark. Instead of quoting a number nobody ran, here's the actual mechanism that caps a 4 vCPU / 8 GB box, how caching changes the arithmetic by orders of magnitude, and exactly how to load-test your own workload.

Navigate

AI Crawlers Explained: GPTBot, ClaudeBot, PerplexityBot, and How to Configure robots.txt

GPTBot, ClaudeBot, PerplexityBot, and a dozen others are already crawling your site. Here's what each one wants, and exactly how to allow, block, or rate-limit them in robots.txt.

Navigate

Server Components vs. Client Components: A Decision Guide for Next.js Apps

Next.js App Router defaults to server components. Learn when to stay server-side, when to cross the "use client" boundary, and how to architect high-performance apps.

Navigate

How to Connect ChatGPT to Your CRM: A Practical Guide

Enable ChatGPT to read and write data directly in your CRM through API integration. Learn two production-ready approaches—function calling and custom actions—with setup patterns and security best practices.

Navigate