dotvitals

The response does not say how long it may be cached

LowConfirmedQuick winhttp.headers.no-cache-control

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

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.

  1. Split your responses into two groups: documents that must be current, and fingerprinted assets whose URL changes with their content.

  2. Send Cache-Control: no-cache for the documents — it stores the response but revalidates before every reuse, so a change is picked up immediately.

  3. Send Cache-Control: public, max-age=31536000, immutable for the fingerprinted assets.

  4. Do not blend the two into one header: no-cache together with a positive max-age is the self-contradiction reported separately by http.headers.cache-directives-conflicting.

  5. For anything user-specific, use private so 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.

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

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.

Open the http headers checkerBuild the fix

Other http headers checks