The response does not say how long it may be cached
What this check looks for
Your page arrives with no caching instructions, so browsers and networks in between invent their own. How long a visitor sees an old copy of the page is currently out of your hands.
Why it matters
Without an instruction, a cache guesses — and its guess can be hours. That is how an updated page keeps showing the old text to some visitors and not others, with nothing to point at as the cause.
When the check passes, your report says: “The response says how long it may be cached”.
What it costs your score
When this check fails it removes 6 points from your HTTP score, before the status, confidence and repeat multipliers are applied. HTTP carries a weight of 7 in the overall score.
It shares the http.caching family ceiling of 12 points: however many findings that family produces, together they cannot remove more than that from HTTP. One underlying problem showing up in several places is still one problem.
- Severity
- low
- Default confidence
- confirmed
- Status when triggered
- warn
- Deduction
- 6 points
- Family cap
- http.caching · 12
- Category
- HTTP
- Module
- Http headers
- Fix owned by
- user
- In the ruleset since
- 2026.09
How to fix it
Send an explicit Cache-Control on every response.
With none, each cache picks its own heuristic lifetime and you cannot predict how stale a visitor's copy is.
Split your responses into two groups: documents that must be current, and fingerprinted assets whose URL changes with their content.
Send
Cache-Control: no-cachefor the documents — it stores the response but revalidates before every reuse, so a change is picked up immediately.Send
Cache-Control: public, max-age=31536000, immutablefor the fingerprinted assets.Do not blend the two into one header:
no-cachetogether with a positivemax-ageis the self-contradiction reported separately byhttp.headers.cache-directives-conflicting.For anything user-specific, use
privateso a shared cache never stores it.
How to confirm it worked
curl -sSI https://‹host›/ | grep -i '^cache-control:' — expect a header, and a single coherent one
Reload the page after changing it and confirm the new content appears without a hard refresh.
# Documents, which must always be revalidated:
Cache-Control: no-cache
# Fingerprinted static assets, whose URL changes when the content does:
Cache-Control: public, max-age=31536000, immutableA named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.
Remediation by platform
location ~* "\.[0-9a-f]{8,}\.(?:css|js|woff2|png|jpg|svg)$" {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / {
add_header Cache-Control "no-cache" always;
}<IfModule mod_headers.c>
<FilesMatch "\.[0-9a-f]{8,}\.(css|js|woff2|png|jpg|svg)$">
Header always set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(html|htm)$">
Header always set Cache-Control "no-cache"
</FilesMatch>
</IfModule>@immutable path_regexp \.[0-9a-f]{8,}\.(css|js|woff2|png|jpg|svg)$
header @immutable Cache-Control "public, max-age=31536000, immutable"
header Cache-Control "no-cache"Cache Rules → Create rule → set Browser TTL and Edge TTL per path, or Rules → Overview → Create rule → Response Header Transform Rule → Set static → Cache-Control. Dashboard paths as at 2026-09.Two rules, not one blended value. Note that an
add_headerinside alocationblock discards everyadd_headerinherited from the enclosing server block, so each location must repeat the headers it needs.immutableis only safe for a URL that changes whenever its content does.Shown because the response identified Apache. Put it in the virtual host or server configuration rather than .htaccess: .htaccess is re-read on every request, is often disabled by AllowOverride None, and runs too late to influence a response the virtual host generates itself. It also needs mod_headers loaded; the IfModule guard means the configuration still parses when it is not, but the header is then simply absent.
Caddy applies the more specific matcher first; the unmatched rule is the fallback.
This applies only to responses that reach visitors through Cloudflare. An origin that is reachable directly — by IP, or through a DNS record that is not proxied — still serves the response measured here unchanged. Setting it at the origin as well covers both paths. A Cache Rule also changes what Cloudflare itself stores, which a Transform Rule does not — decide which of the two you actually mean.
Technical detail
The 200 response from ‹origin› carried no Cache-Control header. ‹detail›
RFC 9111 §4.2.2 permits a cache with no explicit freshness information to calculate a *heuristic* lifetime, and specifically suggests deriving it from Last-Modified — commonly a tenth of the document's age. That is a legitimate cache doing exactly what the specification allows; the problem is that the number is the cache's choice rather than yours, and it differs between browsers, corporate proxies and CDNs.
The fix is two decisions, not one value. Documents that must always be current get no-cache, which permits storage but forces revalidation before reuse. Assets whose URL contains a content hash get public, max-age=31536000, immutable, because a changed asset is a changed URL. Detected server: ‹detected server›.
Standards and references
Test this on your domain
Run the check that produces this finding, on its own, against any domain.
Other http headers checks
- The caching instructions contradict each other
- A text response does not declare its character encoding
- The declared Content-Encoding may not match the body
- A compressible response was sent uncompressed
- The response has a body but no Content-Type
- The response sends obsolete headers, one of them harmful
- No Alt-Svc header advertises HTTP/3
- A response header leaks an internal address
- The response cannot be revalidated without downloading it again
- A compressed response does not vary on Accept-Encoding
- Vary: * makes the response uncacheable by any shared cache