Next.js caching explained: the four layers and how to control them
Most 'Next.js is showing stale data' bugs are one of four caches doing exactly what it was told. Here is the mental model that makes them predictable.
A CDN caches responses at edge locations close to users, keyed by URL plus whatever your Vary header and configuration include. What gets cached and for how long is controlled by Cache-Control; a low hit ratio is almost always caused by missing cache headers, cookies in the cache key, or an over-broad Vary.
It builds a cache key — normally the host plus the path plus the query string, extended by any headers you tell it to vary on — and looks for a stored response. On a miss it fetches from the origin, stores the response according to its caching headers, and serves it. Subsequent requests with the same key are served from the edge.
Different things for static assets, HTML and personalised responses — one blanket policy cannot be right for all three.
| Content | Header |
|---|---|
| Hashed static assets | public, max-age=31536000, immutable |
| HTML that can be slightly stale | public, max-age=0, s-maxage=60, stale-while-revalidate=300 |
| Personalised HTML | private, no-store |
| API responses (public) | public, max-age=30, stale-while-revalidate=60 |
| API responses (per-user) | private, no-store |
s-maxage applies to shared caches like a CDN, while max-age applies to the browser. Setting max-age=0 with s-maxage=60 means the browser revalidates but the CDN serves cached content for a minute — usually exactly what you want for HTML.
It lets the cache serve a stale response immediately while fetching a fresh one in the background. The user never waits for the origin, and the next visitor gets the updated version. It is the closest thing to a free performance win in HTTP caching.
Cache-Control: public, s-maxage=60, stale-while-revalidate=600
# 0–60s : fresh, served from edge
# 60–660s : stale served instantly, refreshed in background
# after 660s : request waits for the originMeasure the hit ratio per content type, not overall. A 95% ratio driven by images can hide 0% on HTML, which is where the latency actually matters.
Yes, for pages that are the same for everyone. Use a short s-maxage with stale-while-revalidate, and mark personalised pages private so they are never shared.
no-cache allows storage but requires revalidation before use. no-store forbids storing the response at all — use it for genuinely sensitive content.
Less than for a global audience, but it still absorbs traffic spikes, terminates TLS closer to users and shields the origin. Usually still worth it.
Cache public GET endpoints with a short TTL and use ETags for conditional requests. Never cache authenticated responses at a shared cache without extreme care around the cache key.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Most 'Next.js is showing stale data' bugs are one of four caches doing exactly what it was told. Here is the mental model that makes them predictable.
LCP is not one number, it is four phases. Optimising the wrong one is why so much performance work produces no measurable change.
Every host looks fine on day one. The differences show up when traffic triples, a region goes down, or you need to leave.
No spam. Just the occasional case study and craft breakdown.