Skip to content
Free SEO Audit

Technical SEO

Caching Headers Explained: max-age, s-maxage, stale-while-revalidate

max-age, s-maxage, and stale-while-revalidate each control a different layer of HTTP caching. What they do, how they interact, and the settings that fit each asset type.

Caching Headers Explained: max-age, s-maxage, stale-while-revalidate — featured image

Cache-Control headers tell browsers and CDNs how long they can reuse a response before checking back with the server, and the three directives that confuse people most — max-age, s-maxage, and stale-while-revalidate — each control a different layer of that decision. max-age sets freshness for every cache in the chain. s-maxage overrides that setting specifically for shared caches like a CDN. stale-while-revalidate defines a grace window where a cache can serve an expired response instantly while fetching a fresh one in the background. Get these three right and repeat visits load close to instantly; get them wrong and you’re either serving stale content or throwing away caching’s entire benefit.

Most sites either cache too little, because someone was worried about serving stale content and set every header to no-cache, or cache too much, because a default CDN configuration cached HTML for a day and nobody noticed until content stopped updating. The fix in both cases is the same: match the cache duration to how often each type of file actually changes.

What does the Cache-Control header actually control?

Cache-Control is a response header the server sends alongside a file, and it’s read by every cache between the origin server and the browser — a CDN edge node, a corporate proxy, and the browser’s own disk cache. The header is a comma-separated list of directives, and the two most load-bearing ones for performance work are max-age and the visibility flags public and private.

DirectiveWhat it controlsApplies to
max-age=NSeconds a response is considered freshAll caches, including the browser
s-maxage=NOverrides max-age for shared caches onlyCDNs, reverse proxies — browsers ignore it
stale-while-revalidate=NExtra seconds a stale response can be served while refreshing in the backgroundModern browsers and CDNs that support it
no-cacheForces revalidation before every use, even if not expiredAll caches
no-storeForbids storing the response at allAll caches
public / privateWhether shared caches are allowed to store the responseAll caches

A common mix-up: no-cache does not mean “don’t cache.” It means “cache it, but check with the server before reusing it every time.” no-store is the directive that actually prevents caching entirely, and it’s the one to use for genuinely sensitive, never-cache content.

What’s the real difference between max-age and s-maxage?

max-age applies uniformly across the caching chain — the browser, any CDN, any proxy in between all treat the response as fresh for that many seconds. s-maxage exists specifically to let you set a different, usually longer, freshness window for shared infrastructure without changing what the end user’s browser does.

A typical pattern: Cache-Control: max-age=60, s-maxage=3600. The browser treats the page as fresh for one minute, so a user hitting back and forward doesn’t cause a fresh network request every time. The CDN, though, holds onto it for an hour, because the CDN sits in front of thousands of users and a one-hour cache dramatically cuts origin server load, while the browser’s shorter window keeps any individual user reasonably current. This split is exactly why s-maxage exists as a separate directive rather than folding into max-age.

How does stale-while-revalidate change the experience after expiry?

Without stale-while-revalidate, the moment max-age expires, the very next request has to wait for a full round trip to the origin server before rendering, even if the content hasn’t actually changed. stale-while-revalidate removes that wait for a defined grace period: the cache serves the expired copy immediately, then fetches a fresh version in the background for next time.

With max-age=120, stale-while-revalidate=300, a response is fresh for two minutes. For the following five minutes after that, requests still get the cached version instantly, while a background fetch quietly updates the cache. Only after the full 420 seconds does a request have to wait on a live fetch. This is the mechanism behind why some CDNs report near-100% cache hit rates even on frequently updated content — the visible response is almost never blocked on a fresh fetch.

How should different asset types actually be cached?

Recommended Cache-Control settings by asset type: long max-age for versioned static assets, short max-age for HTML, no-store for sensitive endpoints, and stale-while-revalidate for API responses

Cache-Control settings by asset type

  • Versioned CSS/JS/images (filename includes a hash)max-age=31536000, immutable — Safe for a year; a content change ships under a new filename.
  • Unversioned static assetsmax-age=86400 to 604800 — A day to a week, balancing freshness against re-fetch cost.
  • HTML pagesmax-age=0, s-maxage=60, stale-while-revalidate=600 — Near-zero for the browser, short CDN window, generous revalidate buffer.
  • API responses used for renderingmax-age=0, must-revalidate or a short s-maxage — Depends on how time-sensitive the data is.
  • Authenticated or sensitive pagesno-store, private — Never cached by shared infrastructure, not stored at all where genuinely sensitive.

The rule underlying all five: cache duration should track how often the underlying content actually changes, not a single sitewide default. Versioned static assets are the easiest win because the filename itself guarantees correctness — cache them for a year and never worry about staleness, since a change produces a new URL rather than overwriting the old one.

How do caching headers affect Core Web Vitals and crawl efficiency?

Aggressive, correct caching directly improves repeat-visit performance, which shows up in Largest Contentful Paint and Time to First Byte on return visits, since the browser skips the network round trip entirely for fresh cached assets. It’s a smaller lever than fixing server response time on the first visit, but it compounds — a returning visitor on a well-cached site experiences a materially faster load than the same visitor on a site issuing no-cache on everything.

On the crawling side, Google’s own documentation on its crawling infrastructure confirms Googlebot supports conditional requests via ETag and Last-Modified headers, and can skip re-downloading a page’s content when the server indicates nothing changed. That’s not a ranking factor directly, but it means well-configured caching headers make each crawl pass more efficient, which matters more on large sites where crawl budget is a real constraint than on a five-page brochure site.

What commonly breaks caching headers in production?

  • A CDN default that caches HTML for 24 hours because nobody overrode it, so published edits don’t appear until the cache naturally expires or someone manually purges it.
  • Cache-busting query strings on static assets that never change (style.css?v=1 left static forever), which defeats the versioned-filename pattern and forces re-validation on every deploy regardless of whether the file changed.
  • Third-party scripts setting their own caching headers that your site has no control over — worth checking when auditing what a given third-party script actually costs you, since some default to no caching at all and re-fetch on every page load.
  • Mismatched Cache-Control between origin and CDN where the origin sends one policy and the CDN configuration silently overrides it with another, leaving nobody sure which one is actually in effect.

The fastest way to catch these is checking response headers directly in browser dev tools on a handful of representative URLs — one static asset, one HTML page, one API call — rather than assuming the CDN dashboard settings match what’s actually being served.

Worth testing directly rather than trusting documentation alone: some CDNs implement stale-while-revalidate differently, or not at all, and silently drop the directive from the response they pass through, which means the behaviour you configured at the origin isn’t necessarily the behaviour a visitor experiences at the edge. A quick way to confirm is a repeat curl request against a live URL just after its max-age window closes — check the response headers for an updated Age value and confirm the content itself didn’t change on that particular request, which tells you the stale-while-revalidate window is actually being honoured rather than assumed.

Frequently asked questions

What’s the difference between max-age and s-maxage?

max-age applies to every cache in the chain, including the browser. s-maxage applies only to shared caches like a CDN or reverse proxy, and overrides max-age for those caches specifically. If s-maxage is absent, shared caches fall back to max-age; if present, browsers ignore s-maxage and use max-age instead.

Does stale-while-revalidate slow down the first request after expiry?

No, that’s the point of the directive. The cache serves the stale copy immediately while fetching a fresh version in the background, so the user gets an instant response instead of waiting for revalidation. The next request after that gets the updated content.

Does caching affect Google’s crawling and indexing?

Yes. Google’s crawling infrastructure supports conditional requests using ETag and Last-Modified headers, and can skip re-downloading unchanged pages when those headers are set correctly, which is more efficient use of your crawl budget. It doesn’t affect rankings directly, but it does affect how quickly and thoroughly Googlebot can revisit a site.

Should HTML pages be cached the same way as CSS and JS files?

No. Static assets like versioned CSS, JS, and images can use long max-age values because their filenames change when the content changes. HTML that reflects current content should use short max-age values or none at all, since serving stale HTML shows visitors outdated information.

Sources

Want this done on your site?

Every PalV’s DM engagement starts with a free audit of your actual website — a 12-point
crawl covering what is blocking indexation, on-page gaps against your primary keywords, speed
findings, and the three to five fixes worth making first. Delivered in two working days. No
payment details, and the findings are yours whether you hire us or not.

Get your free SEO audit
See Web Development services

Written by Palash — founder of PalV’s DM,
an SEO and AI-visibility consultancy in Ahmedabad. Five-plus years in SEO, 1,000+ articles
published, 250+ certifications. Every engagement runs on the same crawl-data-in,
prioritised-actions-out workbook. Full profile and credentials →

Get the audit.
Keep the findings.

Free, no payment details, yours to act on either way.

Get Your Free SEO Audit WhatsApp Us

What you get back

A 12-point audit of your actual site: technical issues blocking indexation, on-page gaps, speed findings, and the three to five fixes we’d make first.

  • 2 daysDelivery
  • 225Checks run
  • ₹0Cost, always