HTTP Caching
Prompt: Make a single-page interactive guide to HTTP caching covering every Cache-Control directive, with diagrams of the browser/CDN/origin flow, a live header-builder form, and a request simulator that picks hit/revalidate/miss based on policy.
Caching is the difference between a fast site and a slow one, but
the rules are subtle. This page walks through every major cache
directive, shows the request/response flow with live diagrams, and
lets you build a real Cache-Control header with a form.
1. The cast: browser, shared cache, origin
Every HTTP response can be cached in two kinds of place: a private cache (the browser, on your laptop) and a shared cache (a CDN or proxy, in front of the origin). Different directives talk to different caches.
2. The freshness lifetime: fresh → stale → gone
A cached response moves through three states. The clock starts the
moment the response is stored. The length of the fresh
window is set by max-age (private) or s-maxage
(shared). After that the response is stale, usable only
with extra rules (stale-while-revalidate,
stale-if-error) or after a successful revalidation.
Fresh response
Returned instantly from the cache. Age response header tells you how old the copy is.
HTTP/1.1 200 OK Cache-Control: max-age=600 Age: 142 ETag: "v3"
Stale response (conditional GET)
Cache asks origin "still good?" with If-None-Match. Origin replies 304 (no body, just "yes") and the cache resets the freshness clock.
GET /thing If-None-Match: "v3" HTTP/1.1 304 Not Modified
3. Every directive, what it does, who reads it
Cache-Control is the modern way to talk to caches. Multiple
directives are comma-separated. Most are response directives
(server → cache); a few are request directives (client →
cache). Here are all of the standard ones.
Response directives
| Directive | Who | What it does |
|---|---|---|
max-age=N | all caches | Fresh for N seconds from the response's date. |
s-maxage=N | shared only | Like max-age, but only for shared caches (CDN/proxy). Overrides max-age there. |
public | all caches | Explicitly permits storage in shared caches, even for normally-private responses (e.g. ones with Authorization). |
private | browser | Only the browser may store it. Forbids CDN/proxy storage. |
no-cache | all caches | You may store, but must revalidate with origin before reuse. Despite the name, it does NOT disable caching. |
no-store | all caches | Do not store. The hardest directive: every request goes through. |
must-revalidate | all caches | Once stale, you may not serve the cached copy on origin failure: return 504 instead. Closes a loophole in max-age. |
proxy-revalidate | shared only | Same as must-revalidate, but only binds shared caches. |
immutable | browser | Body will never change at this URL. Browser skips revalidation even when the user hits reload. |
stale-while-revalidate=N | all caches | For up to N seconds after going stale, serve the stale copy now and revalidate in the background. |
stale-if-error=N | all caches | For up to N seconds after going stale, serve the stale copy if the origin is broken (5xx or unreachable). |
no-transform | all caches | Forbid intermediaries from changing the body (no image re-compression, no minification). |
Request directives
| Directive | What it does |
|---|---|
max-age=N | Client only wants responses no older than N. |
max-stale[=N] | Client tolerates a stale response (optionally up to N seconds past expiry). |
min-fresh=N | Client wants a response that will still be fresh for at least N more seconds. |
no-cache | Force every cache between client and origin to revalidate. |
no-store | Don't store the request or any response to it. Useful for sensitive forms. |
only-if-cached | Return a cached copy or 504. Never hit the network. (Mostly used by offline tooling.) |
Companion response headers you should also know
Age: seconds since this response was generated by origin (added by shared caches).Date: when origin produced the response.Expires: HTTP/1.0 absolute time of expiry. Ignored ifCache-Control: max-ageis present.Vary: list of request headers that vary the response. Without it, a cache can serve the wrong variant (wrong language, wrong encoding) to the wrong user.ETag/Last-Modified: validators used for conditional GETs (see next section).
4. Validators: ETag and Last-Modified
When a stored response goes stale, the cache doesn't have to throw it away and re-download the body. Instead it asks the origin "is the version I have still good?" using one of two validators.
ETag (strong validator)
An opaque tag the origin assigns to a specific representation. Sent back on the next request as If-None-Match.
HTTP/1.1 200 OK ETag: "v3-d41d8cd" GET /thing If-None-Match: "v3-d41d8cd"
Last-Modified (weak validator)
An RFC-1123 timestamp. Sent back as If-Modified-Since. Cheaper to produce but only second-resolution.
HTTP/1.1 200 OK Last-Modified: Tue, 12 May 2026 09:21:00 GMT GET /thing If-Modified-Since: Tue, 12 May 2026 09:21:00 GMT
5. Build a Cache-Control header
Pick directives and see the header (with a one-line plain-English summary) assemble live. The summary will warn you when directives contradict each other.
6. Live simulator: watch the cache decide
Press Send request repeatedly. The browser-side cache will choose between a hit, a conditional GET, and a full miss based on the response's directives and how much time has passed since the last fetch. Time is sped up so you can see freshness expire.
7. Practical recipes
Hashed JS/CSS bundle
Cache-Control: public, max-age=31536000, immutable
URL contains a content hash, so the body for that URL is fixed forever. Browser never needs to revalidate.
HTML entry document
Cache-Control: public, no-cache
Cacheable, but revalidate on every navigation. Pairs with ETag for cheap 304s.
News article behind a CDN
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=86400
Edge holds it for 10 min, browser for 1 min. On expiry, edge serves the old copy and refetches in the background.
Per-user API response
Cache-Control: private, max-age=30
private stops the CDN from leaking one user's data to another. Vary: Authorization is also wise.
Bank statement / one-time token
Cache-Control: no-store
The most restrictive. Combine with Pragma: no-cache for HTTP/1.0 proxies if you're paranoid.
Static error page
Cache-Control: public, max-age=60, stale-if-error=86400
If the origin is on fire for a day, the edge can keep serving the last good error page.
Built as a learning aid. Authoritative source: RFC 9111: HTTP Caching.