HTTP caching and Cache-Control
Fresh versus stale, private versus shared caches, what no-cache really means, and the directives that keep one user's data out of a CDN.
After this lesson you can
- Read a Cache-Control header and say what a browser and a CDN may each do with the response
- Explain the difference between no-cache and no-store
- Keep personalised responses out of shared caches
The fastest request is the one that never reaches your server. HTTP
caching (RFC 9111) lets a response be stored and reused, and
Cache-Control is how the server says what is allowed. Two kinds of
cache read it:
- a private cache belongs to one user — the browser's HTTP cache, or an HTTP client library's cache in a mobile app;
- a shared cache serves many users — a CDN, a reverse proxy, an API gateway.
Fresh and stale
A stored response is fresh while its age is less than its freshness lifetime, and a fresh response can be served without contacting the origin at all. Once it is stale, a cache must normally revalidate it (ask the origin whether it is still current — the next lesson) before using it again.
The lifetime comes from max-age=<seconds>, or for shared caches from
s-maxage, which overrides max-age there. The older Expires header
is ignored when max-age is present.
| Directive (response) | Meaning |
|---|---|
max-age=N | Fresh for N seconds |
s-maxage=N | Fresh for N seconds in shared caches; overrides max-age there |
no-cache | May be stored, but must be revalidated before every use |
no-store | Must not be stored by any cache |
private | Shared caches must not store it; the browser may |
public | Any cache may store it, even where it otherwise could not |
must-revalidate | Once stale, never serve it without revalidating |
The pair everyone confuses: no-cache does not mean "do not cache".
It means "store it if you like, but check with me every time" — which is
cheap when the check comes back 304 Not Modified. no-store is the one
that forbids storing, and it is what sensitive data needs.
function parse(header) { const out = {}; for (const part of header.split(",")) { const [name, value] = part.trim().split("="); out[name.toLowerCase()] = value === undefined ? true : Number(value); } return out;} function decide(cc, shared, age) { if (cc["no-store"] || (shared && cc["private"])) return "do not store"; if (cc["no-cache"]) return "revalidate"; const lifetime = shared && "s-maxage" in cc ? cc["s-maxage"] : cc["max-age"] ?? 0; return lifetime > age ? "serve fresh" : "revalidate";} export function run() { const headers = ["public, max-age=60, s-maxage=300", "private, max-age=60", "no-cache", "no-store"]; const out = {}; for (const h of headers) { const cc = parse(h); out[h] = [30, 90].map((age) => `age ${age}: browser ${decide(cc, false, age)}, CDN ${decide(cc, true, age)}`); } return out;}Keeping personal data out of shared caches
A response that depends on who is asking — /me, an account balance, an
order history — must never be served from a CDN to someone else. Mark it
private (the browser may still cache it) or no-store. HTTP also
protects you by default in one case: a shared cache must not store a
response to a request that carried an Authorization header unless the
response explicitly allows it with public, s-maxage or
must-revalidate.
Vary
If the same URI returns different representations depending on a
request header — Accept-Language, Accept, a version header — the
response must say so with Vary: Accept-Language. A cache then reuses
a stored copy only for a request with the same value of that header,
instead of handing the French version to a German speaker.
What to cache in an API
Public reference data (countries, currencies, product catalogues) can
carry public, max-age of minutes or hours. User-specific reads get
private with a short max-age, or no-cache plus an ETag so clients
revalidate cheaply. Anything secret gets no-store. Writes (POST, PUT,
PATCH, DELETE) are not served from cache.
Common mistakes
Exercise · Pro
What may a shared cache do?
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade