Reference guide · performance · Published 2026-08-15 · 3 min read
HTTP caching headers explained
Understand HTTP caching: Cache-Control and max-age, ETag and Last-Modified revalidation, Vary and stale-while-revalidate, per asset type.
- ·Cache-Control
- ·Revalidation
- ·Vary and revalidate-stale
How HTTP caching works
An HTTP cache, the browser, a proxy, an origin shield, or a CDN edge, stores a response and reuses it when the same URL is requested again, as long as the response's headers say it is fresh. A well-cached site issues repeat visits almost entirely from the cache; a badly-cached one redispatches every asset to the origin. Two directives, Cache-Control and the revalidation headers (ETag, Last-Modified), control the whole game.
Cache-Control
Cache-Control is the primary header. The values combine with a comma:
| Directive | Meaning |
|---|---|
no-cache | Cache the response, but always revalidate with the origin first |
no-store | Never store (auth, transactions) |
max-age=3600 | Consider fresh for 3600 seconds |
public / private | Shared caches can store / only the browser may |
must-revalidate | On a stale response, must revalidate before reuse |
stale-while-revalidate=60 | Serve stale while a background revalidate runs |
A lifetime larger than a request is the point of caching. For a file that changes every release, use max-age in the future and a content hash in the filename so URL and content change together:
Cache-Control: public, max-age=31536000, immutable
Revalidation: ETag and Last-Modified
When a cached response goes stale, the browser sends a conditional request. With ETag, it sends If-None-Match: "<etag-value>"; with Last-Modified, If-Modified-Since: <date>. If the resource is unchanged, the origin answers 304 Not Modified with no body, and the browser keeps the cached copy. 304 responses are cheap; recomposed bodies are not.
| Header | Provided by | Lifetime |
|---|---|---|
ETag | Server generates from the file hash or inode | Precise, byte-for-byte |
Last-Modified | A date the server knows | Coarse (1 second granularity) |
304 Not Modified | Origin, after validation | Removes the re-download |
Prefer ETag when the server can compute it, and otherwise fall back on Last-Modified. If both are present, revalidation uses the stronger.
Vary
Vary tells caches that the response varies with the request headers named. The common cases: Vary: Accept-Encoding keeps a gzip and a brotli version apart, and Vary: Cookie separates a personalized page per user. A Vary header that changes often shuts caching down; the sane default for a shared resource is no Vary, or Vary: Accept-Encoding only.
Pick a policy per asset
| Asset | Cache policy |
|---|---|
| HTML | no-cache so validation catches changes, or short max-age |
| Images, CSS, JS with hashed URLs | public, max-age=31536000, immutable |
| Logged-in / admin responses | no-store |
| Third-party from a different host | Follow its own headers, or use the CDN config |
stale-while-revalidate
stale-while-revalidate serves a stale copy instantly while a background refresh refreshes the cache in the background. It is a big win for data that must stay fast even when the origin is slow, at the price of serving a moment-old version to one user.
Combine these pieces and the happy path: a hashed asset is immutable, a revalidated HTML page costs a 304, and the CDN guide shows where the edge turns that policy into repeat callers never touching the origin.