The caching instructions contradict each other
What this check looks for
The response asks for two different things at once — for example, do not reuse this, and also reuse it for an hour. Each cache resolves the contradiction its own way.
Why it matters
When the instructions disagree, the behaviour you get is whichever one the cache in front of your visitor happens to prefer. You cannot reason about staleness, and a bug that only appears behind one proxy is nearly impossible to reproduce.
When the check passes, your report says: “The caching instructions agree with each other”.
What it costs your score
When this check fails it removes 5 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
- 5 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
Set the caching policy in exactly one place, and make it say one thing.
Contradictory directives mean the policy actually in force is the cache's choice rather than yours.
Find every layer that sets
Cache-Control: the application, the web server, and any proxy or CDN in front of it.Keep one of them — usually the one closest to the application, which knows whether the response is user-specific — and remove the rest.
Choose
no-storeonly for genuinely sensitive responses; chooseno-cachewhen the response may be stored but must be revalidated.Drop
Expiresrather than trying to keep it in step withmax-age; a cache already ignores it whenmax-ageis present.
How to confirm it worked
curl -sSI https://‹host›/ | grep -ci '^cache-control:' — expect 1
curl -sSI https://‹host›/ | grep -iE '^(cache-control|expires|pragma):' — read the result as a single coherent policy
# 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
Conflicts found on the response from ‹origin›: ‹detail›
The common pairs and what actually happens:
no-cachewith a positivemax-age—no-cachewins in practice, so themax-ageis dead text that reads as an intention nobody honours.no-storewithpublicormax-age—no-storeforbids storage entirely, which makes every other directive on the line irrelevant.publicwithprivate— mutually exclusive statements about whether a shared cache may hold the response; a cache that resolves this in your favour is luck, not design.Expiresdisagreeing withmax-age— RFC 9111 §5.3 settles this one cleanly: a cache that understandsCache-Control**must ignore**Expireswhenmax-ageis present. The header is not harmful, it is merely misleading to the humans reading it.
This is nearly always two layers each setting the header: an application and a web server, or an origin and a CDN. 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
- 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 does not say how long it may be cached
- 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