Cache-Control cheat sheet
The Cache-Control values to send for each kind of file, what each directive means, pairs that get mixed up, how validation works, and commands to test it.
In this guide
What to send
| Kind of file | Send |
|---|---|
| CSS, JavaScript, images and fonts with a version or hash in the name | Cache-Control: max-age=31536000, immutable |
| Files with no version in the name | A short max-age, or no-cache |
| An HTML page that is the same for everyone | Cache-Control: no-cache and an ETag |
| The same page, behind a CDN you can purge | Cache-Control: max-age=0, s-maxage=3600 |
| A page that differs per visitor | Cache-Control: private, no-cache |
| A page with data nobody else may see | Cache-Control: no-store |
| API data that may be a minute old | Cache-Control: max-age=60, stale-while-revalidate=300 |
The directives
| Directive | Meaning |
|---|---|
max-age=N | Fresh for N seconds after the server made it. 31536000 is one year |
s-maxage=N | Like max-age, for shared caches only. Wins over max-age there, and browsers ignore it |
no-cache | May be stored, but must be checked with the server before each reuse |
no-store | Do not store it anywhere |
private | Only the visitor's own browser may store it |
public | Any cache may store it, even when the request had an Authorization header |
must-revalidate | Once stale, never reuse it without checking |
immutable | Will not change while fresh, so do not check it |
stale-while-revalidate=N | May be used for N seconds after it is stale, while a fresh copy loads in the background |
stale-if-error=N | May be used for N seconds after it is stale if the server answers with an error |
no-transform | Nobody on the way may change the content |
Directive names are not case sensitive, but lower case is safest. If directives clash, the most restrictive one wins.
Pairs people mix up
| These | Are different because |
|---|---|
no-cache and no-store | no-cache keeps a copy and checks it each time. no-store keeps nothing |
private and "do not cache" | private is about where: a browser may keep it, a shared cache may not |
max-age and s-maxage | The first is for every cache. The second is for shared caches only |
max-age=0, must-revalidate and no-cache | MDN says they mean the same |
public and the default | A response with max-age is already storable by shared caches. public matters for Authorization requests |
Validation
| Header | Direction | Use |
|---|---|---|
ETag | Response | A label for this version of the file |
Last-Modified | Response | When the server thinks it last changed |
If-None-Match | Request | "Do you still have the version with this label?" |
If-Modified-Since | Request | "Has it changed since this date?" |
304 Not Modified | Response | "Keep using your copy." No body is sent |
A normal reload sends max-age=0 with these validators, so you see many 304s. A force reload sends no-cache and downloads everything.
Vary
Vary: Accept-Encoding tells a cache that the answer depends on the compression the visitor accepts. See text compression. Avoid Vary: User-Agent: it has so many values that a cache rarely finds a match. To keep personal pages out of shared caches, use Cache-Control: private, not Vary: Cookie.
Test it
# the policy headers of a file
curl -s -o /dev/null -D - https://example.com/assets/app.3f9c1b.js | grep -i -E '^(cache-control|etag|last-modified|expires|age|vary)'
# does a check end in 304? (use the ETag from the first command)
curl -s -o /dev/null -D - -H 'If-None-Match: "the-etag-value"' https://example.com/assets/app.3f9c1b.js | head -n 1In the Network panel, a file from the browser's cache shows (disk cache) or (memory cache) in the Size column. Untick Disable cache, and open the page by a link or in a new tab, not with Reload.
What Lighthouse checks
The Use efficient cache lifetimes insight lists fonts, images, media, scripts and stylesheets whose lifetime is shorter than 30 days, and estimates the bytes a returning visitor would download again. It skips files marked no-cache, no-store, must-revalidate or private.
Do not
- Send a long
max-agefor an address that never changes its name. - Use
no-storefor a page that only needs to be fresh. It can keep the page out of the back/forward cache. - Leave personal pages without
private. - Let the server, a plugin and the CDN each add their own
Cache-Control. - Put the version in a query string that your CDN ignores.
Check your own page
Run the free audit and open the caching finding. Then run the first curl command on your main CSS file and your home page, and compare the answers with the first table.
Sources
- Cache-Control and HTTP caching on MDN: the directives, validation and the patterns for static files and pages.
- The full HTTP caching headers guide, with its sources, and the Lighthouse 13.5 cache insight as described there.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call