Content Delivery Network Blog

Cache-Control max-age vs s-maxage: Browser and CDN TTLs

Written by Vadym Borovskyi | Nov 2, 2026, 3:48:18 AM

Verdict, evaluated November 2026: set max-age for the browser and s-maxage for the CDN, and keep them deliberately different. In the max age cache control model, max-age applies to every cache, while s-maxage overrides it only in shared caches such as a CDN or origin shield. Fingerprinted assets and video segments get one year plus immutable. HTML gets a browser TTL of 0 and an edge TTL of about 300 seconds with a purge on deploy.

Under RFC 9111, the HTTP caching standard in force as of 2026, a response carrying Cache-Control: public, max-age=60, s-maxage=86400 stays fresh for 60 seconds in a browser and for 86,400 seconds (24 hours) in a CDN. Browsers ignore s-maxage completely. Shared caches let it replace both max-age and Expires.

How max age cache control and s-maxage divide browser and CDN TTLs

A response has one Cache-Control header and two audiences. Private caches (the browser) read max-age. Shared caches read s-maxage first and fall back to max-age only when s-maxage is absent. Two consequences follow from that split. Without s-maxage, your CDN TTL equals your browser TTL. A cache TTL that is safe for a browser is often far too short for an edge that sits in front of thousands of users.

s-maxage also changes revalidation semantics. RFC 9111 says it implies proxy-revalidate, so a shared cache must not serve the object stale after expiry without checking the origin. The one exception is when stale-while-revalidate or stale-if-error explicitly grants that permission. It also allows a shared cache to store a response to a request that carried an Authorization header. Treat that as a data-leak risk, not a convenience.

Directive Browser CDN and shield What it really controls
max-age=N Honored Honored if no s-maxage Freshness in seconds, counted from origin generation (minus Age)
s-maxage=N Ignored Overrides max-age and Expires Edge TTL; implies proxy-revalidate
immutable Firefox and Safari honor it Generally ignored Suppresses reload revalidation while fresh
stale-while-revalidate=N Chromium and Firefox Widely honored Serve stale for N seconds while refetching in the background
no-cache Revalidate every use Revalidate every use Cancels the benefit of s-maxage
private May store Must not store Per-user responses

The only directive that targets the CDN without touching the browser is s-maxage, so any edge TTL longer than the browser TTL depends on it (or on a vendor header such as CDN-Cache-Control).

CDN-Cache-Control, defined in RFC 9213, and the older Surrogate-Control header do the same job with vendor-specific support. If you use them, confirm whether your CDN strips them before the response reaches the client.

Why the Age header can silently cancel your browser cache TTL

Browsers calculate remaining freshness as max-age minus current age, and current age includes the Age header the CDN sends. When s-maxage is much larger than max-age, most edge hits arrive at the browser already stale. The browser stores the response but revalidates it on next use.

Here is a worked case. Take Cache-Control: public, max-age=600, s-maxage=86400 on a popular image, and assume requests arrive evenly across the object's 24-hour edge lifetime. A response is already stale in the browser whenever its Age exceeds 600 seconds. That happens for 1 minus 600/86,400 of hits, which is 99.3%. In practice the browser TTL is close to zero, and every repeat view costs a conditional request that returns 304 Not Modified. That round trip still adds latency, even though it moves no bytes.

Real popularity curves are not uniform, so treat 99.3% as an illustration of the mechanism, not a measurement. The fix depends on intent. If you want browsers to hold the file, raise max-age toward s-maxage. If you want fast invalidation, accept the 304s and make sure ETag or Last-Modified validators are stable across origin servers.

The staleness budget: worst-case cache TTL after a deploy

This is the original framework of this article. It gives the longest time a user can see the old version of a URL after you change it, with S as the s-maxage and M as the max-age.

  • Age passed through intact: budget = max(S, M). The browser's expiry is anchored to when the origin generated the response, so it cannot outlive whichever TTL is longer.
  • An intermediary resets or strips Age: budget = S + M. Common causes are a reverse proxy behind the CDN, an edge rewrite or a misconfigured shield tier. Each tier that resets Age adds its TTL again.
  • Purge on deploy succeeds: budget = M. A purge clears edges, but it can never reach a browser.

The rule that follows is simple. Browser TTL is the only part of your staleness you cannot recall, so max-age should never exceed how long you can tolerate wrong content. For HTML at max-age=0, s-maxage=300 with purge on deploy, the budget is effectively zero. Without the purge it is 300 seconds.

What does Cache-Control immutable actually do?

Cache-Control immutable, defined in RFC 8246, tells the browser that the response body will never change at this URL. The browser therefore skips revalidation while the response is fresh, even when the user presses reload. It does not extend the cache TTL. Without max-age it does nothing, and it is unsafe on any URL whose content can change.

Firefox and Safari honor the directive. Chromium-based browsers do not implement it, but since 2017 they have not revalidated subresources on a normal reload anyway, so the outcome is similar. CDNs generally ignore immutable and take their TTL from s-maxage or max-age. Reserve the directive for content-hashed file names and media segments, where the URL itself is the version.

Safe cache TTL defaults for HTML, APIs, images and video segments

Content Cache-Control value Staleness budget Invalidation
HTML documents public, max-age=0, s-maxage=300, stale-while-revalidate=60 0 with purge, 300 s without Purge on deploy
Hashed JS and CSS bundles public, max-age=31536000, immutable Not applicable (new URL per version) Change the file name
Public read API (GET) public, max-age=0, s-maxage=30, stale-while-revalidate=30, stale-if-error=600 30 to 60 s TTL expiry
Personalized API private, no-store 0 None needed
Images, versioned URL public, max-age=31536000, immutable Not applicable Change the URL
Images, unversioned URL public, max-age=3600, s-maxage=604800, stale-while-revalidate=86400 1 h with purge, 7 days without Purge by URL
HLS and DASH segments (VOD and live) public, max-age=31536000, immutable Not applicable New segment names on re-package
VOD playlists and MPD public, max-age=300, s-maxage=86400 5 min with purge Purge on re-package
Live media playlists public, max-age=2, s-maxage=2 (for 6 s segments) 2 s TTL expiry

The pattern behind every row is the same: content whose URL changes with its bytes gets a one-year TTL plus immutable, and content that changes in place gets a short browser TTL and a longer, purgeable edge TTL.

HTML and APIs

Use max-age=0 rather than no-cache on HTML. Pairing no-cache with s-maxage forces the CDN to revalidate every request, so the edge TTL buys nothing. For APIs, stale-if-error=600 lets the edge answer for 10 minutes during an origin outage. That is often the most valuable directive in the header.

Live video

Keep the live playlist TTL at or below half the target segment duration, so 2 to 3 seconds for 6-second segments. Cache 404 responses for segments for no more than 1 to 2 seconds. Otherwise an edge that asks too early will keep serving the miss after the segment exists. For LL-HLS, the blocking playlist reload parameters must be part of the cache key.

Trade-offs and failure modes of long s-maxage values

Purge dependency. A long s-maxage is only safe if purges are reliable and wired into your deploy pipeline. If purge is a manual console step, your real staleness budget is S.

Origin load spikes. Short edge TTLs on HTML move load back to the origin every 300 seconds per edge location. Origin shield and request coalescing collapse those refreshes into roughly one origin fetch per object per TTL.

Vary fragmentation. Vary: User-Agent or Vary: Cookie can multiply variants and push hit ratio down regardless of TTL. Normalize request headers at the edge or vary on a small derived header.

CDN overrides. Many CDNs apply minimum, default and maximum TTLs that clamp your header. Amazon CloudFront's managed caching-optimized policy, per vendor documentation, uses a 24-hour default when the origin sends no Cache-Control. When no header and no default exist, RFC 9111 permits heuristic freshness, typically 10% of the time since Last-Modified. A file untouched for 30 days can therefore be cached for 3 days without any instruction from you.

When headers cannot be changed at the origin, edge-side cache rules are the escape hatch. BlazingCDN zones are set up together with an engineer and support custom cache rules, purge and cache warm-up through the API, along with origin shield and request coalescing. That combination keeps a short HTML s-maxage cheap at the origin.

How to verify browser and CDN TTLs on your own stack

  • Request the same URL twice through the CDN and compare the Age header. It should rise between requests and should never reset to zero on a hit. A reset means some tier is stretching your staleness budget to S + M.
  • In browser devtools, reload a page and count 304 responses for images. A high count with long s-maxage values is the Age trap from the worked example.
  • Compare the edge cache-status header against origin request rate per URL. If origin hits per object exceed one per s-maxage window per shield, coalescing or the cache key is broken.

FAQ: max age cache control and s-maxage

Do browsers ignore s-maxage in Cache-Control?

Yes, browsers ignore s-maxage because RFC 9111 limits it to shared caches such as CDNs, proxies and origin shields. The browser uses max-age alone, minus any Age header the CDN sends. That means a response with max-age=0 and s-maxage=3600 is revalidated by the browser on every use, while the edge serves it from cache for an hour.

Should HLS segments use Cache-Control immutable?

Yes, HLS and DASH media segments should use max-age=31536000 with immutable, because a published segment never changes at its URL. Playlists are different. Live media playlists need a TTL of about half the segment duration, and VOD playlists need a short browser TTL with a purgeable edge TTL. Never mark a playlist immutable, since players must see updates.

What cache TTL should HTML pages have behind a CDN?

A safe default for HTML is max-age=0 with s-maxage=300 and stale-while-revalidate=60, plus a purge on every deploy. Browsers revalidate each view cheaply through ETag, while the edge absorbs traffic for five minutes. Avoid no-cache on HTML behind a CDN, because it forces the shared cache to revalidate every request as well.

What happens when max-age is shorter than the CDN's Age header?

When the Age header exceeds max-age, the browser treats the response as already stale and revalidates on next use. With max-age=600 and s-maxage=86400, about 99% of edge hits arrive stale under evenly spread traffic. Raise max-age if you want browser caching, or accept 304 revalidations when fast invalidation matters more.

Audit your cache TTLs and test them on a pull CDN

This week, export your top 20 URLs by bytes served. Record the Cache-Control, Age and cache-status headers for each one. Then compute two numbers per URL: the staleness budget (max of S and M, or S plus M if Age resets) and the share of edge hits that reach the browser already stale (1 minus M divided by S). Any URL where the budget exceeds your tolerance for wrong content, or where the stale share is above 90% without a reason, needs a header change. Ship those changes before you touch anything else in the cache layer.

If your workload is heavy static delivery, such as video segments, installers or images, the BlazingCDN Anycast CDN page describes the pull-caching setup. It also covers the 14-day testing period, run on real production traffic, where you can repeat this audit.