Two caching policies, and never one in between
There are two correct caching policies and nothing useful between them. A fingerprinted asset should be kept for a year and never checked again; a document should be checked every time. Almost every caching problem is a response wearing the other one's policy.
The short answer
There are two correct caching policies and nothing useful in between. Content-addressed assets, the ones with a hash in the filename, get a year and immutable. HTML documents get no-cache, which means revalidate every time rather than never cache. Anything in the middle serves stale content for a period nobody chose.
Two policies, and never one in between
The reason there are only two is the filename. If an asset's URL contains a hash of its contents, a change produces a new URL, so the old one can never be stale and there is no reason to ever revalidate it. If a URL is stable, like every page on your site, then its contents can change at any time and the browser has to ask.
# fingerprinted assets: /assets/app.4f2c1a.js
Cache-Control: public, max-age=31536000, immutable
# HTML documents: the URL stays the same, the content does not
Cache-Control: no-cache
# anything private to one visitor
Cache-Control: private, no-store
# an API response that may be reused briefly, then refreshed in the
# background while the stale copy is still served
Cache-Control: public, max-age=60, stale-while-revalidate=600The naming of the second one is the single most misunderstood thing in this area. no-cache does not mean do not cache. It means cache it, and revalidate before reusing it, which is exactly what you want for a document: the browser keeps a copy, asks whether it has changed, and gets a small 304 back if it has not. The directive that genuinely means do not keep a copy is no-store, and it is for responses containing somebody's personal data.
immutable is worth adding to the first policy. Without it, a browser will still revalidate a long-cached asset on a reload, and with it that request is skipped entirely.
What each mistake looks like from the outside
Getting the policies the wrong way round produces two failures that feel like completely different problems.
A document cached for a long time means visitors and crawlers keep seeing an old page after you have published a new one, and nothing you do at the origin reaches them until the time expires. This is the worse of the two, because it is invisible from the inside: you deployed, your own browser was force-refreshed, the page looks right to you, and it is wrong for everybody who visited yesterday. Any caching header on HTML longer than a few minutes is worth questioning.
An asset with no caching policy means every visit re-downloads your CSS and your JavaScript, which is a real cost on a slow connection and an entirely avoidable one. It is the safer mistake, and it wastes the largest single performance win available for a repeat visit.
Two other conditions to know about. A response with no Cache-Control at all is subject to heuristic caching, where the browser invents a freshness lifetime from the Last-Modified date: this is why a page you never gave a policy to can still be served from a cache. And ETag or Last-Modified is what makes revalidation cheap, so a document with no-cache and no validator has to be re-sent in full every time it is checked.
If your assets are not fingerprinted, fix that before you touch the headers.
A long cache on /assets/app.css is a trap: you cannot deploy a change without waiting out the expiry or renaming the file. The version query string that people reach for instead, app.css?v=3, mostly works and creates a parameterised URL for every version you have ever shipped.
Content hashing in the filename is what makes the year-long policy safe, and every modern build tool does it by default. The headers in this guide assume it.
A policy per response type, not per site
The useful finding compares the headers across the different kinds of thing you serve: documents, fingerprinted assets, unfingerprinted assets, uploads, API responses. Reading one response tells you about that response, and the mistakes in this area are always a mismatch between two of them.
This one needs the full crawl
Caching directives arrive as headers, and the finding worth having compares them across your documents, your assets and your API responses rather than reading one.
The instant search on this site audits a single page, so rather than show you a verdict it cannot support, this guide sends you to the place the check actually runs.
What caching does for a crawler
This is the part of the check that belongs in an SEO audit rather than in a performance one, and it is not the part most guides mention.
A crawler sends conditional requests. It remembers the ETag or the Last-Modified date from last time and asks whether anything has changed, and a 304 Not Modified answer costs a fraction of a full response. On a large site that is the difference between a crawler getting through your catalogue in a day and getting through part of it.
Which makes a validator on your documents worth having for a reason unrelated to browsers. A site that answers every conditional request with a full page is asking for its whole crawl budget to be spent re-reading pages that did not change, and the pages that did change are the ones that wait.
The related failure is a Last-Modified date that is always now, which is what a dynamically rendered page reports by default. Every conditional request then gets a full response, and the validator is worse than useless because it costs a comparison and never saves a transfer. The same honesty problem applies to a lastmod in a sitemap, for the same reason.
Does this affect AI search?
Marginally, and through the same conditional-request mechanism. Anything that fetches your pages repeatedly benefits from being able to ask whether they changed, and a site that supports that properly is cheaper to revisit. Cheaper to revisit means revisited more often, which means a fresher copy of your content wherever it is being held.
The staleness risk runs the other way and is worth naming. A document cached aggressively is a document whose old version circulates, and in this setting the old version may be what gets quoted back to somebody weeks later. Correcting a page you got wrong is slower when the wrong one has a long lifetime.
Neither of those is a reason to prioritise this check. They are reasons to prefer no-cache on documents, which is what the first section recommends anyway.
Why the delivery pair splits across two ratings
Compression next door is rated Important and this is a Refinement, which needs an explanation given they live in the same configuration file.
Compression helps every visitor on every visit, including the first one, and there is no wrong way to do it. Caching helps the second visit onward, does nothing for somebody arriving for the first time, and has a genuinely wrong configuration that is worse than having none. A finding whose benefit is conditional and whose fix can backfire belongs lower on the scale.
The rating also reflects that the common state is fine. Most frameworks ship sensible defaults now: fingerprinted assets with a long policy, documents without one. The finding tends to be a gap on one route rather than a site with no strategy, and a gap on one route is a small thing.
Where it would rate higher is the specific case in the second section: a long cache on your HTML. That is not a refinement, it is a page you cannot correct, and if a report shows it, treat it as the most urgent item in this group.
Reading the policy on each kind of response
Five requests, one per category, and the comparison between them is the check.
show() {
printf '%-40s %s\n' "$1" \
"$(curl -s -I "$1" \
| grep -iE '^(cache-control|etag|last-modified|age):' \
| tr -d '\r' | paste -sd' | ' -)"
}
show https://example.com/ # expect no-cache
show https://example.com/guides/sharpening # expect no-cache
show https://example.com/assets/app.4f2c1a.js # expect a year, immutable
show https://example.com/assets/app.css # unfingerprinted: careful
show https://example.com/api/products # short, or private
# does a conditional request actually save anything?
tag=$(curl -s -I https://example.com/ | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r')
curl -s -o /dev/null -w 'revalidated: %{http_code}, %{size_download} bytes\n' \
-H "If-None-Match: $tag" https://example.com/The last block is the one worth running. A 304 with a handful of bytes means revalidation is working. A 200 with the whole page means your validator is not being honoured, and every conditional request from every browser and every crawler is being answered with a full transfer.
Questions this check raises
- What does no-cache actually mean?
- Not "do not cache". It means the response may be stored but must be revalidated with the server before reuse, which is usually a cheap 304. The directive that prevents storage entirely is no-store, and it is rarely what anyone wants outside authenticated responses.
- Why use immutable on hashed assets?
- Because the filename changes when the content does, so the URL can never serve different bytes. immutable tells the browser not to revalidate even on a reload, which removes a round trip per asset for returning visitors. It is only safe with content-addressed filenames.
- What is wrong with a one-hour cache on HTML?
- It means a correction can be up to an hour late for anyone who visited recently, and you cannot tell who is affected. Documents change unpredictably, so revalidation is the right model: the request still happens, and a 304 costs almost nothing.