HTTP Caching
Cache-Control, ETags, and revalidation — the cheapest performance win that exists.
40 minDifficulty 3/5performance · apiAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
A browser re-downloads a 2MB unchanged JavaScript bundle on every single page load, even though the file is byte-for-byte identical to the one it fetched a minute ago — not because the browser is dumb, but because nothing in the server's response told it this file could be safely reused. HTTP caching is entirely opt-in via response headers; silence means "fetch it again next time."
The mental model
Two independent questions govern every cached response: 'how long can I use this without asking again at all?' (freshness, controlled by `Cache-Control`) and 'once it's stale, is the version I have still actually current?' (validation, controlled by `ETag`/`Last-Modified`). A response can skip re-fetching entirely for the freshness window, and even after that window, often skip re-DOWNLOADING via a cheap validation check.
How it works
Cache-Control: max-age sets the freshness window
`Cache-Control: max-age=3600` tells the browser 'you may use this cached response for up to 3600 seconds without contacting the server at all' — no network request happens during that window, which is the fastest and cheapest possible outcome, faster even than a validation round trip.
ETags let a stale cache validate cheaply instead of re-downloading
Once the freshness window expires, a request WITH `If-None-Match: <etag>` asks the server 'has this changed since I last had etag X?' — if it hasn't, the server responds `304 Not Modified` with NO body at all, and the browser reuses its existing cached copy. This is dramatically cheaper than re-transferring the whole resource, at the cost of one small round trip instead of zero.
Content-addressed URLs make max-age arbitrarily long and safe
A filename like `app.a3f92b1.js`, where the hash is derived from the file's actual content, guarantees that if the content ever changes, the URL changes too — a new deployment produces a new filename entirely. This means `Cache-Control: max-age=31536000, immutable` (cache for a full year) is completely safe: the URL itself is the version identifier, so 'stale content at this URL' is structurally impossible.
Private vs shared caching matters for anything user-specific
`Cache-Control: private` means only the end user's OWN browser may cache this response — a shared cache (a CDN, a corporate proxy) must not store it, because doing so would risk serving one user's personal data (an account page, say) to a different user who happens to hit the same URL through that shared cache.
The mechanism
The first response includes both a freshness duration and a validator (ETag). While fresh, the browser serves entirely from cache with no network involvement. Once stale, the browser sends a conditional request with the stored validator; if the server confirms nothing changed, it returns an empty 304 response, and the browser continues using its cached copy — only a genuinely changed resource triggers a full re-download.
sequenceDiagram participant B as Browser participant S as Server B->>S: GET /app.js (first request) S->>B: 200 OK, Cache-Control: max-age=3600, ETag: "abc123" Note over B: cached, fresh for 3600s B->>S: GET /app.js (after 3600s, now stale) Note over B: sends If-None-Match: "abc123" S->>B: 304 Not Modified (no body) Note over B: reuses existing cached copy
What people get wrong
- A 304 Not Modified response means the request failed or nothing was returned.
- A 304 is a SUCCESSFUL outcome — it means the browser's cached copy is confirmed still valid, and the browser correctly proceeds to use it, exactly as if the full resource had been re-downloaded, just without the cost of re-transferring the body. Seeing 304s in network logs and assuming they represent a problem misses that they're the caching system working exactly as intended, saving real bandwidth.
- Without any Cache-Control header, the browser will cache the response using some sensible default duration.
- Browsers apply various heuristics in the ABSENCE of explicit caching headers, but relying on unspecified heuristic behaviour is unpredictable and varies across browsers — an explicit Cache-Control header is the only reliable way to control caching behaviour. Assuming 'no headers means reasonable default caching' leads to inconsistent behaviour across browsers and makes performance debugging much harder, since the actual caching behaviour isn't explicitly declared anywhere.
- Setting a long max-age is always risky because you might need to update the content before it expires.
- This risk disappears entirely with content-addressed (hashed) filenames — since any content change produces a new URL, a long max-age on the OLD url is completely safe, because that URL's content genuinely never changes again. This is exactly the pattern behind modern build tools' output filenames, and not understanding it leads to unnecessarily short cache durations on assets that could safely be cached for a year.
When not to use it
- The content is truly dynamic and different on every single request, like a real-time stock price.
- `Cache-Control: no-store`, explicitly preventing any caching at all — this is the case caching genuinely shouldn't apply to, unlike merely infrequently-changing content.
- The response contains data specific to the logged-in user.
- `Cache-Control: private`, ensuring shared caches (CDNs, proxies) never store it, even if the user's own browser reasonably caches it for their own subsequent requests.
Terms
- Cache-Control
- — The response header controlling caching behaviour, including freshness duration (max-age) and scope (public/private/no-store).
- ETag
- — An opaque identifier for a specific version of a resource, used in conditional requests to check if a cached copy is still valid without re-transferring the content.
- 304 Not Modified
- — A response indicating a conditional request's cached copy is still valid, sent with no body, letting the client continue using its existing cache.
- Content-addressed URL
- — A URL containing a hash derived from the resource's actual content, guaranteeing any content change produces a different URL entirely.
In an interview
Why is it safe to set Cache-Control: max-age=31536000 (one year) on a file named app.a3f92b1.js, but not on a file named app.js?
- app.a3f92b1.js's filename is derived from its content hash, so if the content ever changes, the URL changes too — this specific URL's content is permanently frozen
- app.js's URL never changes even when its content does, so a long cache duration would serve stale content after a real update, with no way for the browser to know to re-check
Can you recall it?
What's the difference between what Cache-Control: max-age and an ETag each accomplish, and why do both exist rather than just one?
Connected ideas
Also part of
This idea matters in more than one area — which is usually why it matters.