For build-fingerprinted static assets such as `/static/app.8f3a1c.js`, teams often send `Cache-Control: public, max-age=31536000, immutable`. Explain what `immutable` adds on top of the long `max-age`, and what breaks if you use that header on a non-fingerprinted URL.
answer
- max-age handles navigation; immutable handles reload
- RFC 8246: won't change while fresh
- fingerprint in the URL makes it true by construction
- hard reload still wins (request no-cache)
- HTML index = no-cache, assets = immutable
basics
~20 sThe long max-age already stops normal re-fetching. immutable (RFC 8246) additionally tells the browser not to send a conditional revalidation request when the user reloads the page — the case where browsers otherwise revalidate anyway. On a non-fingerprinted URL it is unsafe: clients can pin the old bytes for a year with no way to purge them.
solid answer
~60 s`max-age=31536000` makes the asset fresh for a year, so no request is made during normal navigation. But browsers historically treat a user-initiated **reload** as a reason to revalidate everything on the page, sending conditional requests that mostly return `304`. On a page with dozens of assets that is dozens of round trips paid for nothing. **`immutable`** (RFC 8246) says the representation will not change during its freshness lifetime, so the client should skip that revalidation even on reload. It changes reload behaviour, not expiry behaviour. The contract only holds because the URL is content-addressed: change the file, and the fingerprint changes, so it is a *different* URL. There is nothing to invalidate. On a stable URL like `/static/app.js` the same header is a trap. Once shipped, browsers may hold those bytes for a year and never ask again; you cannot purge a browser cache, so the only escape is changing the URL — which is what fingerprinting was for. Serve the HTML that references the assets with `no-cache` so new fingerprints are picked up immediately.
code
http · 9 linesGET /index.html
HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "build-9931"
GET /static/app.8f3a1c.js
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Content-Type: text/javascriptgo deeper
Know the pattern: hashed filenames get a one-year cache, HTML does not. Understanding why is the next step.
Explain that immutable suppresses revalidation on reload and that the fingerprint is what makes the promise safe.
Discuss the unrecoverable failure mode on stable URLs, the HTML/asset split, and the build-pipeline invariants that must hold for the promise to be true.
Own it as a release-safety property: any deploy step that mutates built content without rehashing is a defect class, so make fingerprint integrity a pipeline invariant rather than a convention.
## The two-layer pattern Modern asset delivery separates a **mutable index** from **immutable content**. The HTML document is short-lived or revalidated on every navigation; the assets it references carry content hashes in their filenames and are cached effectively forever. Deploying a new build writes new asset URLs and updates the HTML; clients pick up the new HTML, follow the new URLs, and the old files simply stop being referenced. No cache invalidation is required anywhere — the problem is designed out rather than managed. ## What max-age alone does not cover `max-age=31536000` (one year, the practical maximum) makes the stored response fresh for the whole period, so during ordinary browsing the client never contacts the origin. What it does not cover is user-initiated reload. Browsers treat a reload as a signal that the user suspects something is wrong, and historically revalidate subresources — issuing conditional requests with `If-None-Match` or `If-Modified-Since`. Those almost always return `304 Not Modified` with no body, so bandwidth is small, but the *latency* is not: on a high-RTT mobile link, forty conditional requests is a visible delay for zero benefit when the content is provably unchanged. ## What immutable adds RFC 8246 defines `immutable` as an assertion that the representation **will not change while it is fresh**. A client that understands it should not send a conditional request for that resource even on a normal reload. It has no effect on when the response expires, no effect on the freshness calculation, and no effect once the response is stale — at that point the client revalidates normally. It is purely a reload-behaviour optimisation, and it is safe only because the fingerprint makes the assertion true by construction. A hard reload (typically Ctrl/Cmd+Shift+R) still bypasses the cache: browsers send `Cache-Control: no-cache` on the request, and request directives outrank the stored response's `immutable`. Users always retain an escape hatch. ## What goes wrong on a stable URL Suppose `/static/app.js` — no hash — is served with `max-age=31536000, immutable`. Ship a bug, then ship the fix, and you discover the fix cannot reach anyone who loaded the page: - The browser considers its copy fresh for a year, so it makes **no request at all**, conditional or otherwise. There is no request for your CDN or origin to answer differently. - Purging the CDN does nothing, because clients are not asking the CDN. - Changing the response headers does nothing, because delivering new headers requires a request. The only real remedies are changing the URL (`app.js?v=2`, which is what fingerprinting does properly) and waiting. Query-string versioning works but is weaker: some intermediaries historically treated query strings specially, and it is easy to forget one reference. A content hash in the path is the robust form. ## Getting the HTML layer right The pattern collapses if the HTML is also cached aggressively, because clients then keep resolving the old asset URLs. Serve the entry document with `Cache-Control: no-cache` (store it, but revalidate every time — cheap, since a `304` costs no body) or a very short `max-age`. Then a deploy propagates in one round trip. The same reasoning applies to any manifest or index: service worker scripts, `manifest.json`, and API discovery documents should be revalidated, while the versioned things they point at can be immutable. ## Operational checks Before turning this on, verify three things. That the build genuinely changes the fingerprint for every content change — including for source maps, fonts and CSS referenced from CSS. That nothing rewrites asset content in place at deploy time, such as an environment-variable substitution step that mutates a built file without rehashing it. And that your CDN is not configured to strip or shorten the header. A single asset whose content changes under a stable fingerprint produces a class of bug that is essentially undebuggable from the server side, because the affected clients never make a request you can observe.
- If a browser never sends a request for an immutable asset, how does a user ever escape a bad cached copy?A hard reload sends `Cache-Control: no-cache` as a request directive, which overrides the stored response and forces revalidation, so the user has an escape hatch. From the operator's side there is none: you cannot purge a browser cache, so the intended remedy is to change the URL, which the fingerprint does automatically on the next build.
- Is query-string versioning (`app.js?v=3`) equivalent to a hashed filename?Functionally similar, since the query string is part of the cache key, but weaker in practice. Some older intermediaries and proxy configurations treat query-bearing URLs specially or drop the query from the cache key, and manual version bumps get forgotten in a way build-generated content hashes do not. A hash in the path derived from file content is the more reliable form.
- Why serve the HTML with `no-cache` rather than a short `max-age`?`no-cache` guarantees the document is checked on every navigation, so a deploy is visible on the next page load, while the stored copy still saves the payload whenever the origin answers 304. A short `max-age` leaves a window in which clients keep resolving old asset URLs, and choosing its length is a guess. For a small document, the revalidation round trip is the cheaper tradeoff.
saying these in an interview costs you the question
- Thinking `immutable` extends or changes the freshness lifetime
- Applying the header to non-fingerprinted URLs and assuming a CDN purge can undo it
- Caching the HTML entry document as aggressively as the assets
- Assuming `immutable` blocks a hard reload
- Believing a CDN purge reaches copies already stored in browsers