Measured in your browserWe advise on speed. We practice it.Loaded just now · real numbers from this visit, not a lab score.
Page loaded
First byte
DOM ready
First paint
Largest paint
DNS lookup
TLS handshake
Transferred
Saved by compression
Requests

Cache-Control is the most load-bearing header on the web and the most misread: no-cache permits caching, private is a security boundary, and two directives most developers ignore do more work than all the rest. This guide covers what each directive actually means and the combinations worth memorizing.

The freshness pair: max-age and s-maxage

max-age=N declares the response fresh for N seconds in any cache; s-maxage=N overrides it for shared caches (CDNs, proxies) only, and its existence is what makes the two-layer TTL strategy from the TTL guide expressible in one header. Three facts prevent the common surprises: freshness is counted from response generation, and the Age header tells you how much is already spent when an edge hands you a cached copy; s-maxage silently implies proxy-revalidate semantics on expiry for shared caches; and an expired response is not deleted — it becomes revalidation-eligible, which is why expiry produces cheap 304s rather than full refetches when validators (next section's ETag) are in place. If you set only one number for everything, you have declared browsers and your CDN to have identical freshness needs; they almost never do.

The misread trio: no-cache, no-store, private

The three most dangerous directives are dangerous because their names lie. no-cache does not prevent caching — it permits storage but requires revalidation before every use; paired with ETags it is the correct, efficient setting for always-fresh-but-cacheable resources like an SPA's index.html. no-store is the one that actually forbids storage anywhere, and it belongs on genuinely sensitive responses — with the corollary that it also disables the back-button cache and every efficiency, so applying it site-wide "to be safe" is a performance incident with a compliance costume. private permits browser caching but forbids shared caches — it is a security boundary, and the incidents where one user's page reaches another almost always involve private content missing the directive while an edge rule cached by path. Rule of thumb: personalization decides between private and no-store; nothing personalized ever ships with public.

The modern extensions: immutable and the stale pair

Three younger directives carry outsized value. immutable tells browsers a fresh response will never change, suppressing the revalidate-on-reload behavior that otherwise floods conditional requests on user refreshes — essential on hashed assets, meaningless (and unsafe) elsewhere. stale-while-revalidate=N lets a cache serve an expired response for up to N seconds while refetching in the background, deleting expiry latency from the user experience; stale-if-error=N does the same when the origin errors, buying you availability. Both are RFC 5861 and both are now honored broadly at the CDN layer — CloudFront added origin-directive support in 2023, Cloudflare's implementation went fully asynchronous in early 2026, Fastly has offered header-level control for years — with per-platform behavior differences that the serve-stale guide covers in depth. The stale pair is the cheapest resilience most estates haven't deployed.

Combinations worth memorizing

Five recipes cover most of a site. Hashed static assets: public, max-age=31536000, immutable. Cacheable HTML with event purging: public, max-age=60, s-maxage=86400, stale-while-revalidate=60, stale-if-error=86400 — browsers stay near-fresh, the edge runs long under purge control, expiry and origin errors are both invisible. Always-fresh shell files: no-cache plus a strong ETag. Shared API reads: public, s-maxage=15, stale-while-revalidate=30 with browser caching off (max-age=0) if clients must not hold data. Personal responses: private, no-store as your paranoid default, relaxed to private, max-age=0, must-revalidate only where the browser holding a revalidatable copy is acceptable. Write these into your framework's response helpers so routes opt into a recipe by name — cacheable_html() beats every developer remembering directive semantics at 6 p.m.

Header hygiene: conflicts, defaults and audits

Three hygiene rules keep the header honest in production. Resolve the conflict stack: Expires and Pragma are legacy — Cache-Control wins where both exist, so delete the old headers rather than maintaining contradictions; and know your CDN's override order (console TTL rules vs origin headers — who wins is per-platform configuration, and unexamined it is where mystery TTLs come from). Set an explicit default: frameworks and servers emit surprising implicit headers; a middleware that stamps no-store on anything that didn't explicitly choose a recipe converts forgotten routes from silent risks into visible slowness someone fixes. And audit reality quarterly: crawl your own key URLs, record the headers actually received at the edge and after it, and diff against the table from the TTL guide — configuration drift between what you wrote and what ships is normal, and only measurement, per the hit-ratio discipline, catches it.

Get the free assessmentMore analysis