Charts · 25

Why they are still seeing the old version

You shipped the fix, you can see it, they cannot, and a hard reload fixes it for you and not for them. Nothing is broken: their browser has a stored response it believes is still fresh, so it never asked. Here are five sets of headers run against the same six visits, with the deploy in the middle, so you can see exactly which visit finds out and which one does not.

Cache-Control: max-age=3600ETag: "a3f1c9"URL: /index.html

HTML with max-age=3600. You deploy the fix and nobody sees it for an hour. This is the single most common caching bug in production.

VisitWhat the browser doesStatusTransferredTime
First visitNothing stored yet, so a full request.20084 KB245 ms
Clicks through, 30 s laterStill fresh: 30 s old against max-age=3600. No request leaves the machine, so a change on the server is invisible until it expires.200 (memory cache)00 ms
Comes back half an hour laterStill fresh: 1800 s old against max-age=3600. No request leaves the machine, so a change on the server is invisible until it expires.200 (disk cache)03 ms
You deploy the fix, they reloadthe fix is live on the serverStill fresh: 1900 s old against max-age=3600. No request leaves the machine, so a change on the server is invisible until it expires.They still see the old version.200 (disk cache)03 ms
They hard-reload (Cmd+Shift+R)A hard reload sends no-cache upstream and ignores every stored response, which is why it always fixes it for you and never for them.They see the fix.20084 KB245 ms
An hour after their first loadStill fresh: 1799 s old against max-age=3600. No request leaves the machine, so a change on the server is invisible until it expires.They see the fix.200 (disk cache)03 ms

Times assume one 4G round trip of 170 ms and 75 ms to send 84 KB, the same model as chart 11. What matters is the shape: a fresh hit costs nothing and can be wrong, a 304 costs one round trip and is always right, a full response costs both.

Notes

Freshness is the whole mechanism: a stored response may be reused without contacting the server while its age is under max-age, and only when it is stale does a validator come into play. no-cache does not mean do not store; it means store it but always revalidate before use, which is what you want on HTML. no-store is the one that means do not keep it. immutable tells the browser not to revalidate even on an explicit reload, and it is safe only on a URL whose content genuinely cannot change, which is what a build hash guarantees: a new build is a new URL, so the old one never needs invalidating. A conditional request carries If-None-Match (from the ETag) or If-Modified-Since (from Last-Modified) and can come back as a 304 with no body, which costs a round trip and nothing else. stale-while-revalidate lets the browser serve the stale copy immediately and refresh in the background, so the user sees the previous version exactly once. Vary is the fourth header and the one that silently breaks things: it lists the request headers that form part of the cache key, so a response that varies on Accept-Encoding is stored separately per encoding, and one that varies on Cookie is effectively uncacheable. A hard reload sends Cache-Control: no-cache on the request, which is why it always works for you and never for anyone else. Sources: RFC 9111 (HTTP Caching), sections 4 and 5; RFC 5861 (stale-while-revalidate); Chrome DevTools network documentation.