dotvitals

The caching instructions contradict each other

LowConfirmedQuick winhttp.headers.cache-directives-conflicting

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 the whole score is calculated

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.

  1. Find every layer that sets Cache-Control: the application, the web server, and any proxy or CDN in front of it.

  2. Keep one of them — usually the one closest to the application, which knows whether the response is user-specific — and remove the rest.

  3. Choose no-store only for genuinely sensitive responses; choose no-cache when the response may be stored but must be revalidated.

  4. Drop Expires rather than trying to keep it in step with max-age; a cache already ignores it when max-age is 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

The configuration to publish
# 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, immutable

A 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

nginx
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;
}
Apache
<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>
Caddy
@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"
Cloudflare
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_header inside a location block discards every add_header inherited from the enclosing server block, so each location must repeat the headers it needs. immutable is 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-cache with a positive max-ageno-cache wins in practice, so the max-age is dead text that reads as an intention nobody honours.
  • no-store with public or max-ageno-store forbids storage entirely, which makes every other directive on the line irrelevant.
  • public with private — mutually exclusive statements about whether a shared cache may hold the response; a cache that resolves this in your favour is luck, not design.
  • Expires disagreeing with max-age — RFC 9111 §5.3 settles this one cleanly: a cache that understands Cache-Control **must ignore** Expires when max-age is 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.

Open the http headers checkerBuild the fix

Other http headers checks