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.
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.
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.
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.
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.
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.
| 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.
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.
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.
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.
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.
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.
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.
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.
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.