How HTTP caching headers work
HTTP caching headers tell browsers and shared caches such as CDNs whether they may store a response and how long it stays fresh. Cache-Control sets those rules. Validators such as ETag and Last-Modified let a cache check whether a stale copy is still current, and the server answers 304 Not Modified if it is.
A fresh response is reused without contacting the server at all. A stale one needs revalidation, which costs a round trip but usually no body.
Without explicit rules, caches may still store a response and guess a lifetime. RFC 9111 gives 10 percent of the time since the Last-Modified date as a typical guess. A file last changed 10 days ago could then be reused for a day without any check, so set the rules yourself.
Cache-Control directives and what each one does
A response's Cache-Control header is a comma-separated list of directives. These are the ones most sites need:
- max-age=N keeps the response fresh for N seconds after the server generated it. One year is max-age=31536000.
- s-maxage=N overrides max-age for shared caches such as CDNs and proxies. Browsers ignore it.
- no-cache allows storing the response but requires revalidation with the server before every reuse.
- no-store forbids storing the response in any cache.
- private allows only the browser to store the response, so shared caches such as CDNs must not keep a copy.
- public allows shared caches to store a response they normally would not, such as one to a request that carried an Authorization header.
- immutable (RFC 8246) says the response will not change while fresh, so supporting browsers skip revalidation even when the user reloads.
- stale-while-revalidate=N (RFC 5861) allows a cache to serve a stale response for up to N seconds while it fetches a fresh one in the background.
- must-revalidate forbids serving a stale response without checking with the server first.
The difference between no-cache and no-store
Despite its name, no-cache does not prevent caching. The browser keeps the copy and checks with the server before each use, which costs a round trip but often transfers no body.
The no-store directive prevents caching entirely, so every visit downloads the full response again. Use it for responses that should never be written to disk, such as banking pages or one-time tokens. Use no-cache when you only need every reuse to be current.
Expires is the older HTTP/1.0 header for freshness and takes an absolute date. When a response also has Cache-Control: max-age, caches ignore Expires.
Revalidation with ETag, Last-Modified and 304
An ETag is an identifier the server assigns to one version of a resource. It can be a content hash, a revision number or any value that changes when the content changes. Last-Modified gives the date the resource last changed, to the nearest second.
When a stored response goes stale, or carries no-cache, the browser sends a conditional request. It puts the ETag in an If-None-Match header, or the date in If-Modified-Since. If nothing changed, the server replies 304 Not Modified with no body, and the browser reuses its copy with refreshed headers.
ETags can be strong ("v42") or weak (W/"v42"). The W/ prefix goes outside the quotes and marks versions that are equivalent in meaning but may differ byte for byte.
Behind a load balancer, every server must generate the same ETag for the same content. Otherwise a request that reaches a different server misses the match and downloads the full body again.
GET /api/products HTTP/1.1
Host: example.com
If-None-Match: "v42"
HTTP/1.1 304 Not Modified
ETag: "v42"
Cache-Control: no-cacheCache busting with hashed filenames
A file cached with a long max-age stays in browsers until it expires, and the server has no practical way to update those copies. The way around this is to change the URL whenever the content changes.
Build tools do this by putting a hash of the file contents in its name, such as app.3f9a2c1b.js. A new build produces a new name, the HTML references it, and browsers fetch it as a new resource. Unchanged files keep their names and stay cached.
This works only if the HTML itself is revalidated. If the HTML is cached for a day, visitors keep receiving old pages that point at old asset names. A version query string such as ?v=2 also works, but a content hash changes automatically and only when the file does.
Sensible headers for HTML, static assets and private data
Most sites need three or four policies, set per path or file type in the server or CDN configuration. The example below covers the usual cases.
For HTML on a busy site, one variation is Cache-Control: public, max-age=0, s-maxage=300. Browsers revalidate on every visit, while the CDN can serve its copy for five minutes; purge the CDN on each deploy.
To see which policy a response got, open the Network panel in your browser's developer tools. Responses served from the cache are labeled there, and a 304 status shows a revalidation.
# HTML pages: always check for a newer version
Cache-Control: no-cache
ETag: "8c1f2e"
# Hashed static assets (app.3f9a2c1b.js, logo.8d2e41.svg)
Cache-Control: public, max-age=31536000, immutable
# Pages or API responses with one user's data
Cache-Control: private, no-cache
# Sensitive responses that must not be stored
Cache-Control: no-store