HTTP Caching
Date: 2026-08-16
Telling the browser it may reuse a response instead of asking again. The whole design is a trade between freshness and round trips — and the standard resolution is to cache aggressively and change the filename when the content changes.
What it is
HTTP caching is a set of response headers instructing browsers and intermediaries how long a response may be reused, and how to check whether it’s still valid.
Two distinct mechanisms, and knowing which one you’re using matters:
- Freshness — the response may be used with no request at all until it expires
- Validation — the response has expired, so the browser asks “has this changed?” and may get a cheap
304 Not Modified
Freshness saves an entire round trip. Validation saves the download but still costs the round trip — which on mobile is most of the cost.
Cache-Control
Cache-Control: public, max-age=31536000, immutable
Cache-Control: private, max-age=0, must-revalidate
Cache-Control: no-store
| Directive | Means |
|---|---|
max-age=N | Fresh for N seconds |
s-maxage=N | Same, but for shared caches (CDN) only — overrides max-age there |
public | Shared caches may store it |
private | Browser only. Use for anything user-specific |
no-cache | Store it, but always revalidate before use. Not “don’t cache” |
no-store | Never store. The actual “don’t cache” |
immutable | Won’t change before it expires — skip revalidation even on reload |
stale-while-revalidate=N | Serve stale for up to N seconds while refreshing in the background |
no-cache is the most misread directive in HTTP. It permits caching and requires revalidation. no-store is the one that means what people think no-cache means.
The standard pattern
/assets/app.4f9a2b1c.js Cache-Control: public, max-age=31536000, immutable
/assets/main.7d3e8a45.css Cache-Control: public, max-age=31536000, immutable
/product/wool-socks Cache-Control: public, max-age=0, s-maxage=300,
stale-while-revalidate=86400
/account Cache-Control: private, no-store
Fingerprinted assets cached for a year. The hash in the filename changes when the content does, so a new deploy produces a new URL and there is no invalidation problem — the old file simply stops being requested. immutable stops the browser revalidating even on a manual reload.
HTML cached briefly at the CDN, not in the browser. max-age=0 means the browser always checks; s-maxage=300 lets the CDN serve it for five minutes without touching the origin. That combination gives you fast TTFB and quick content updates at once — Time to First Byte, CDN Caching.
Personal pages not stored at all. An account or basket page in a shared cache is a data breach.
stale-while-revalidate
The most useful directive people don’t use:
Cache-Control: max-age=60, stale-while-revalidate=86400
For 60 seconds the response is fresh. For the next 24 hours it’s served immediately from cache while being refreshed in the background. The user never waits for revalidation; the content is at most slightly stale.
Ideal for content that changes but where a minute-old version is harmless — category pages, navigation, content blocks.
Validators
When freshness expires:
ETag: "a4f2c9" strong content identifier
Last-Modified: Wed, 13 Aug 2026 09:14:00 GMT
The browser sends If-None-Match or If-Modified-Since; the server answers 304 with no body if unchanged. ETags are more precise; Last-Modified has one-second resolution.
Watch ETags behind multiple servers — if each generates a different ETag for identical content, every request is a cache miss. This is a classic load-balanced deployment bug and it silently disables caching.
What it does to your data
Caching changes what you can measure, which matters when analysing performance:
- Warm visits are much faster than cold ones, so a bimodal distribution in Real User Monitoring usually means cache hit versus miss, not two kinds of user — Percentiles in Performance
- Lab tests are cold by default and therefore pessimistic against a returning-visitor population — Field vs Lab Data
- Cache hit rate is a first-class metric. A CDN at 60% hit rate means 40% of users are waiting on your origin
Traps
- Caching HTML in the browser with a long
max-age. Users get a stale page and clearing it requires them to hard-refresh, which they won’t no-cacheintended asno-store, leaving private data in a shared cache- Forgetting
Vary. A response varying byAccept-Encoding,Acceptor cookie must say so, or a cache serves the wrong variant to the wrong user - Unfingerprinted assets with long expiry. Now you have an invalidation problem, and purging a CDN is not the same as purging a million browsers
- Cookies defeating CDN caching. Many CDNs skip caching for requests carrying cookies. A single analytics cookie on a static asset path can disable the whole layer