Cache-Control
Cache-Control is the HTTP header that governs caching: whether a response may be stored, by whom (browser only, or shared caches like CDNs too), for how long, and what happens when it goes stale. If you configure exactly one header on your site, make it this one.
The directives that matter
| Directive | What it does |
|---|---|
max-age=3600 | Fresh for 3600 s — applies to browsers and shared caches. |
s-maxage=86400 | Overrides max-age for shared caches (CDNs) only — see s-maxage. |
public | May be stored by any cache, even when other signals suggest otherwise. |
private | Browser may cache it; CDNs must not. For personalized responses. |
no-cache | May be stored, but must be revalidated (e.g. via ETag) before every use. Not "don't cache". |
no-store | The actual "don't cache": nothing may be written to any cache. |
immutable | Won't change during its lifetime — browsers skip revalidation on reload. For fingerprinted assets. |
stale-while-revalidate=60 | Serve stale for up to 60 s while refreshing in the background — see stale-while-revalidate. |
A pattern that covers most sites
# HTML — short in the browser, long at the edge, purge + warm on deploy
Cache-Control: public, max-age=60, s-maxage=86400
# Fingerprinted assets (app.3f9a2c.js) — cache forever, never revalidate
Cache-Control: public, max-age=31536000, immutable
The split matters: browsers you can't purge, CDNs you can. A short
max-age keeps browsers honest; a long s-maxage gives the CDN
a high hit ratio; a
purge plus re-warm on deploy keeps content
correct.
What Cache-Control can't do
It sets the rules, not the reality. A CDN may cap or override your TTLs, ignore directives it doesn't support, or evict your object early under cache pressure. And some CDNs need explicit opt-in before they cache HTML at all — Cloudflare famously marks HTML DYNAMIC by default no matter what Cache-Control says. Verify what actually happens at the edge with our cache checker.