Cache-Control header generator
Cache-Control has no validator — a contradictory combination is not an error, just a silently applied precedence rule — so the lint pass is the part that matters.Start from a common case
Your header
Cache-Control: public, max-age=31536000, immutableWhat this header actually does (1)
immutable is only safe on a content-addressed URL
It tells caches not to revalidate even on a manual reload, which means a wrong file is unfixable at the edge — you cannot purge a browser cache. Only use it where the filename contains a hash of the contents (app.4f2b9c.js, or a versioned media segment), so that changing the bytes changes the URL.
Directive by directive
public- Any cache may store this, including shared caches like a CDN — even when the request carried an Authorization header, which would otherwise make the response private by default.
max-age=31536000- Fresh for 1 year after it was generated. Any cache may serve it without contacting the origin until then.
immutableRFC 8246 extension- The body will never change while fresh, so a cache should not revalidate even when the user presses reload. This is the directive that stops a reload from firing hundreds of pointless conditional requests.
Setting it
location ~* \.(js|css|woff2|m4s|ts)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}Why contradictory directives are so common
Nothing rejects a bad Cache-Control. There is no schema, no build step, and no warning in any server — a header that stores nothing and a header that caches for a year look equally valid on the way out. The two most common failures are no-store arriving alongside a long max-age, usually because a framework default and an explicit override both applied, and immutable on a URL whose contents can change in place.
The second one is the expensive mistake. immutable suppresses revalidation even on a manual reload, and no purge reaches a browser cache. Ship the wrong bytes at a stable URL with a year-long lifetime and the only fix is waiting, or changing the URL.
The shapes worth knowing
Hashed assets and media segments take public, max-age=31536000, immutable, because the filename changes when the bytes do. HTML usually wants max-age=0 with a longer s-maxage, so the edge holds it while the browser always checks. A live HLS media playlist needs a lifetime shorter than its segment duration, or the edge will serve a manifest that is missing the newest segments.
To see what a URL is doing today rather than what it should do, the cache header checker reads the real response and grades it. For what a cache miss costs, the CDN cost calculator puts a number on the origin egress a low hit ratio produces.
Frequently asked questions
- What is the difference between no-cache and no-store?
- no-store forbids writing the response to storage at all — nothing is kept, anywhere. no-cache does allow storage, but requires the cache to revalidate with the origin before serving what it holds. Despite the name, no-cache still caches: it just never serves without checking first, which is far cheaper than re-downloading because an unchanged resource comes back as a bodyless 304.
- What is the difference between max-age and s-maxage?
- max-age applies to every cache, including the user's browser. s-maxage applies only to shared caches such as a CDN, and overrides max-age for them. The pair exists so you can hold something at the edge for hours while keeping the browser copy short-lived — max-age=0, s-maxage=3600 is the common shape for HTML.
- When is it safe to use immutable?
- Only when the URL changes whenever the content changes — a filename containing a content hash, like app.4f2b9c.js, or a versioned media segment. immutable tells caches not to revalidate even when the user presses reload, so a wrong file cannot be corrected by purging: browsers that already hold it will keep it until max-age expires. On a URL whose content can change in place, it is a trap.
- Does Cache-Control: no-store, max-age=31536000 cache anything?
- No. no-store forbids storage outright, so there is nothing for max-age to describe a lifetime for. The header looks like aggressive caching and delivers none — it is one of the most common accidental combinations, and usually appears when a framework default and an explicit override are both applied to the same response.
- What does stale-while-revalidate do?
- It defines a window after expiry during which a cache may serve the stale copy immediately and fetch a fresh one in the background. The user never waits for the origin, which removes the latency spike that otherwise hits whoever happens to request a resource the moment it expires. It is defined in RFC 5861 as an extension, so support varies — most major CDNs implement it, and browser support is narrower.
- Do I still need ETag or Last-Modified if I set Cache-Control?
- Yes, for anything that is revalidated rather than simply re-downloaded. Cache-Control decides how long a response stays fresh; a validator decides what happens after that. Without one, an expired entry must be fetched in full, whereas with one the cache sends a conditional request and usually gets a 304 with no body at all.