HTTP caching is controlled by a handful of response headers, and small
differences between directives have large effects on performance and
correctness. This reference covers the Cache-Control directives, the
validators (ETag, Last-Modified), the freshness headers (Expires, Age),
and Vary, with notes on how browsers and CDNs interpret each.
How it works
A cache decides whether to reuse a stored response by checking freshness then
validation. Freshness comes from Cache-Control: max-age (or the older
Expires date): while the response age is below the limit, it is served without
contacting the origin. The current age is reported in the Age header.
Once a response goes stale, the cache validates it. If the origin sent an
ETag or Last-Modified, the cache sends a conditional request
(If-None-Match / If-Modified-Since). The origin replies 304 Not Modified
with no body if nothing changed, saving bandwidth, or 200 with fresh content.
Shared caches (CDNs, proxies) honour extra directives — s-maxage, public,
private, proxy-revalidate — that let you cache differently at the edge than
in the browser. Vary tells the cache which request headers produce different
responses so it stores a separate variant per value.
Common directive combinations
Fingerprinted static assets (JS, CSS, images with hash in filename)
Cache-Control: public, max-age=31536000, immutable
Cache forever (one year). The filename changes when content changes, so there is no risk of serving stale content. immutable tells the browser to skip revalidation even on a hard reload — it was served fresh and won’t change. CDNs and browsers both benefit.
HTML pages
Cache-Control: no-cache
ETag: "abc123"
no-cache stores the response but requires a revalidation check before serving it. This ensures users always get the newest page without paying the full download cost — if the ETag matches, the server returns 304 Not Modified and the browser reuses its copy.
API data that can be slightly stale
Cache-Control: public, max-age=60, stale-while-revalidate=30
Serve from cache for 60 seconds. For the 30 seconds after that, keep serving the stale response while fetching a fresh copy in the background. Users always get a fast response; the stale window is visible only to very frequent callers.
Sensitive responses (auth, personal data)
Cache-Control: no-store
Never store the response, not even temporarily. No CDN, proxy, or browser cache will keep a copy.
The Vary header and CDN caching
Vary lists request headers that affect the response, so the cache stores a separate entry per unique combination of values. Vary: Accept-Encoding is safe and common — the cache stores a gzipped and an uncompressed version per URL. Vary: User-Agent is destructive: it creates a separate cache entry for every browser and device string, effectively disabling CDN caching entirely.
If you serve different content for mobile vs desktop, use separate URLs or a cookie-based flag that you control rather than Vary: User-Agent.
Tips
- Keep
Varyminimal —Accept-Encodingis fine,User-Agentdestroys hit rates. - Use
stale-while-revalidateandstale-if-errorto hide origin latency and outages from users. s-maxageoverridesmax-agefor shared caches only — use it to cache aggressively at the CDN edge while keeping browsers conservative.- The
Ageresponse header tells you how old the cached copy is (in seconds since the origin generated it); useful for diagnosing stale-cache issues.