Cheat Sheet · HTTP Caching
growing
Cache-Control
Which directive to ship — and where the one-time-value case forces your hand.
The common path, not the whole law: Cache-Control directives compose more freely than any tree implies. Start here, then reach for the reference at the end. The companion essay, The Header That Can't Be Cached, is the why.
Which directive do I ship?
-
Does the body carry a per-response one-time value?
a CSP nonce, a one-time CSRF token rendered into the HTML
- yes
no-storethe nonce lands here — a stored body is a reused nonce - no
Is the response identical for every user?
- no
privatebrowser may store; addno-cacheto revalidate each use — unless the body holds a one-time value, which is the yes branch above - yes
Is the URL content-addressed / fingerprinted?
- yes
max-age=31536000, immutablehashed assets — cache a year, never revalidate - no
max-age=…, must-revalidateorno-cacheto force a check on every reuse
- yes
- no
- yes
no-cache is not no-store
The trap at the center of the topic. The names are a historical accident.
no-cache— store it, but revalidate before reuse. A copy is kept; it just can't be served without checking first.no-store— keep no copy at all. The only directive that forbids storage, anywhere, by any cache. This is the one a nonced page needs.
Who stores it
- private cache — the browser. What it stores, only that reader sees.
- shared cache — a CDN, proxy, gateway. What it stores, everyone behind it sees.
privateands-maxagespeak to these only.
How long it's fresh
max-age=N— fresh forNseconds from generation, for every cache.s-maxage=N— overridesmax-agefor shared caches only; the usual split iss-maxage=3600, max-age=60(CDN an hour, browser a minute). Once stale unders-maxage, a shared cache must revalidate — it impliesproxy-revalidate.stale-while-revalidate=N(RFC 5861) — may serve the stale copy forNseconds while refetching in the background;stale-if-erroris its outage-tolerant sibling.- No header at all — not "no caching": caches may assign a lifetime heuristically (RFC 9111 §4.2.2 suggests ~10% of the time since
Last-Modified). If you care, say so explicitly.
The two extremes
must-revalidate— once stale, may not be served again until rechecked.immutable— won't change while fresh, so don't even revalidate on reload. What you put onapp.9f3c.js. (Firefox and Safari honor it; Chrome never implemented the directive — it changed reload behavior instead.)
Why a nonce forces no-store
A nonce is unique per response; a cache's whole job is to reuse a stored response. private moves where the copy lives; no-cache governs when it's rechecked — a 304 still serves the stored (stale-nonce) body. Neither stops reuse. Only no-store does.
Keep your cache anyway
- Static / content site — vouch by hash (
sha256-…), not nonce. Stable across responses, so it caches forever. Shippublic, max-age=…, immutable. - Need nonces + caching — inject the nonce at the edge (CDN worker). Origin stays cacheable; only the last hop is unique.
Don't bother
Pragma: no-cacheon a response — never specified, so no cache is required to honor it (RFC 9111 §5.4). A deprecated HTTP/1.0 request header; shipCache-Control.publicby reflex — responses are shared-cacheable by default once fresh.publicoverrides a prohibition; it doesn't grant permission you have.
Outside HTTP's jurisdiction
- A service worker's Cache API ignores
Cache-Controlentirely — your code stores and expires entries itself. - The back/forward cache restores a snapshot of the live page; history navigation may show expired content regardless of directives.
Varydecides the cache key (which request headers make responses distinct); wrongVaryis its own class of bug this page doesn't cover.
The reference
RFC 9111 is the law; MDN's Cache-Control page is the readable version; the companion essay is the why.