Cache-Control
request & response headercaching
Directives that tell browsers and CDNs whether, where and for how long a response may be cached.
Cache-Control is the main caching header. On a response, it says who may store the response and how long it stays fresh before it must be revalidated. On a request, it lets the client ask caches to revalidate or bypass stored copies — which is what browsers send on a hard reload.
Syntax
Cache-Control: <directive>[=<value>], <directive>…
Defined in RFC 9111 §5.2 — Cache-Control.
Values
| max-age=<seconds> | fresh for this many seconds in any cache |
|---|---|
| s-maxage=<seconds> | freshness for shared caches (CDNs) only; overrides max-age there |
| public | any cache may store it, even for authenticated requests |
| private | only the user's browser may store it, not CDNs |
| no-cache | may be stored, but must be revalidated before every use |
| no-store | must not be stored anywhere |
| must-revalidate | once stale, never serve without revalidating |
| immutable | will never change — don't revalidate, even on reload |
| stale-while-revalidate=<seconds> | serve a stale copy for this long while refreshing in the background |
Examples
Cache-Control: public, max-age=31536000, immutable
fingerprinted static assets such as app.3f2a1c.js
Cache-Control: no-cache
HTML: always revalidate, so deploys show up on the next load
Cache-Control: private, no-store
personal or sensitive data
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
API responses cached briefly on a CDN
Good to know
no-cachemeans 'revalidate before use', not 'don't cache'. Useno-storeto keep a response out of caches.- Without Cache-Control, browsers guess a freshness lifetime from
Last-Modified— always set it explicitly on HTML. s-maxageonly affects shared caches; CDNs such as Cloudflare and Vercel honour it.Expiresis the HTTP/1.0 predecessor; when both are present,max-agewins.
Further reading: HTTP caching explained
How to set Cache-Control
// Fingerprinted assets: cache for a year
app.use("/assets", express.static("dist/assets", { maxAge: "1y", immutable: true }));
// HTML and API responses: always revalidate
res.set("Cache-Control", "no-cache");return Response.json(data, {
headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300" },
});
// Next.js already sends long-lived immutable caching for /_next/static files.location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / {
add_header Cache-Control "no-cache" always;
}<FilesMatch "\.(js|css|woff2|png|jpg|svg)$"> Header set Cache-Control "public, max-age=31536000, immutable" </FilesMatch> <FilesMatch "\.html$"> Header set Cache-Control "no-cache" </FilesMatch>
// Ask caches along the way to revalidate
await fetch("/api/data", { headers: { "Cache-Control": "no-cache" } });
// Or control the browser's own HTTP cache for this request
await fetch("/api/data", { cache: "no-store" });Check it
Fetch only the response headers and filter for Cache-Control. In the browser, it's under Response Headers in DevTools → Network.
curl -sI https://example.com/ | grep -i '^cache-control:'
To compare response headers across many URLs at once, use the HTTP Status Checker.
Frequently asked questions
What's the difference between no-cache and no-store?
no-cache lets caches store the response but makes them check with the server — usually a cheap 304 — before each use. no-store forbids storing it at all.
What's the longest sensible max-age?
One year (31536000 seconds) is the convention for immutable, fingerprinted assets. Anything that can change at the same URL needs a much shorter lifetime, or no-cache with an ETag.