Cache-Control headers
Cache-Control tells browsers and CDNs how long to keep a response. Use immutable + max-age=31536000 for fingerprinted assets and short or no-cache for HTML.
What it is
Cache-Control is an HTTP response header that governs how, where, and for how long a response may be stored and reused. It is defined in RFC 9111 and replaces the older Expires and Pragma headers.
Cache-Control: public, max-age=31536000, immutable
Key directives:
public— any cache may store it (CDN, proxy, browser).private— only the user’s browser may store it.no-cache— store, but revalidate before reuse.no-store— do not store at all.max-age=<seconds>— fresh for this many seconds.s-maxage=<seconds>— same, but only for shared caches (CDNs).immutable— promise that the body will never change, so the browser can skip revalidation.stale-while-revalidate=<seconds>— serve stale while fetching a fresh copy in the background.
Why it matters
A correct cache policy is the cheapest possible performance win. Repeat visitors download nothing for cached assets. CDNs absorb traffic before it reaches your origin. Without it, every page view re-fetches the same CSS, JS, and images. With it, the second visit can be near-instant.
no-cache is also a privacy tool: it forces revalidation of HTML so users see your latest content, while still allowing 304s.
How to implement
Fingerprinted assets — cache forever. If the URL contains a content hash (app.4f3a2b.js), the body cannot change. Cache aggressively:
Cache-Control: public, max-age=31536000, immutable
immutable matters: without it, Firefox revalidates on reload even with max-age set.
HTML — short or no cache. Pages change often and rarely have fingerprinted URLs. Use:
Cache-Control: public, max-age=0, must-revalidate
or, for highly dynamic pages:
Cache-Control: no-store
Pair with ETag so revalidation is cheap.
Per-user content — private. Anything personalised needs private to prevent shared caches from leaking one user’s data to another.
Set Vary correctly. If the response varies by Accept-Encoding or Accept-Language, set Vary accordingly. Missing Vary causes a CDN to serve gzip to a client that asked for brotli.
Common mistakes
- No
Cache-Controlat all. Browsers then use heuristic caching, which is unpredictable. max-age=31536000on HTML. Users get stale pages and you can’t fix bugs without changing the URL.no-cachemistaken for “do not cache”. It only means “revalidate first”. Useno-storeto actually prevent storage.- Missing
Vary: Accept-Encodingwhen serving brotli/gzip via a CDN. - Fingerprinted URL without
immutable— Firefox will still revalidate on reload.
Verification
curl -I https://example.com/app.4f3a2b.js— confirmCache-Controlis set.- DevTools → Network → Size column shows “(memory cache)” or “(disk cache)” for cached resources.
- Webhint and Lighthouse flag inefficient cache policy.
Related topics
Sources & further reading
- RFC 9111 — HTTP Caching — IETF
- MDN — Cache-Control — MDN
- MDN — HTTP caching — MDN
- MDN — Vary — MDN