Website problems
Missing cache headers
`Cache-Control` tells browsers and intermediate caches how long a response may be reused and under what conditions. Without it, a browser applies heuristics or refetches, so a returning visitor downloads stylesheets, scripts and images that have not changed since their last visit. Correct caching is the single cheapest performance improvement available on most sites, because it removes requests entirely rather than making them faster — and the mechanism that makes aggressive caching safe is fingerprinted file names.
The directives that matter
`max-age=<seconds>` is how long the response may be reused without asking the server. This is the main control.
`public` and `private` say whether shared caches — a CDN, a corporate proxy — may store the response. Anything personalised must be `private`, or it risks being served to the wrong person.
`no-cache` does not mean do not cache. It means the cache may store the response but must revalidate before reusing it. `no-store` is the directive that means do not store at all.
`immutable` tells the browser the file will never change at this URL, so it need not revalidate even on a reload. It is what makes fingerprinted assets free on repeat visits.
`stale-while-revalidate` lets a cache serve a stale response while fetching a fresh one in the background, which removes the wait without serving old content indefinitely.
Fingerprinting is what makes long caching safe
The objection to long cache lifetimes is always the same: if a file is cached for a year and you need to change it, visitors keep the old one.
Fingerprinting removes the objection. Build tools emit `app.4f3a9c.js` where the hash derives from the content, so changing the content changes the URL — and the new URL has never been cached.
That splits assets into two groups with two policies. Fingerprinted files get `max-age=31536000, immutable`: a year, never revalidated. HTML gets a short lifetime or revalidation, because its URL is stable and its content changes.
Without fingerprinting, long caching is genuinely dangerous and short caching is the only safe option — which is why sites without a build step end up refetching everything.
Revalidation, and why ETags are not enough
An `ETag` or `Last-Modified` header lets a browser ask 'has this changed?' and receive a 304 with no body if it has not.
That saves the bytes and not the request. On a high-latency connection, thirty 304 responses still cost thirty round trips before the page can finish.
So revalidation is the right tool for content that changes unpredictably and the wrong one for static assets, where `immutable` avoids the conversation entirely.
A common misconfiguration is `no-cache` on everything, which sounds cautious and produces a revalidation request for every asset on every page view.
How to check it yourself
`curl -I https://example.com/assets/app.js` and read `Cache-Control`. Its absence is the finding.
Check each class of resource separately: the HTML document, a fingerprinted script, an image, a font. They should not all have the same policy, and frequently they all do.
In the Network panel, reload and look at the `Size` column. Entries reading `(disk cache)` or `(memory cache)` were not fetched; entries showing a transfer size were.
Do a repeat visit rather than a hard reload. A hard reload deliberately bypasses the cache and tells you nothing about what a returning visitor experiences.
A policy that works
Fingerprinted static assets: `Cache-Control: public, max-age=31536000, immutable`.
Images and fonts that are not fingerprinted: a long `max-age` with revalidation, or fingerprint them too.
HTML: `Cache-Control: no-cache` — store it, revalidate it — so a deployment is picked up immediately while the response is still cached for the revalidation case.
Anything personalised or authenticated: `private, no-store` unless you are certain, because a shared cache serving one user's page to another is the worst failure in this list.
API responses: decide per endpoint. `stale-while-revalidate` suits data that is useful slightly stale; `no-store` suits anything containing personal information.
How VeriFixScan detects it
`performance.cache_control` reads the `Cache-Control` header of the documents and resources the crawl retrieves and reports the ones sent without a policy, with the URL of each.
`performance.static_cache` reports the lifetimes applied to static assets specifically, which is where a short or absent policy costs the most on repeat visits.
`performance.cdn_cache_status` reads the cache-status headers a CDN adds, which distinguishes a response served from the edge from one that reached the origin.
`infrastructure.cache_state` reports what sits in front of the origin, which decides where the policy should be set.
Frequently asked questions
- Does no-cache mean do not cache?
- No. It means store the response but revalidate before reusing it. The directive that prevents storage is no-store.
- Is caching an asset for a year safe?
- Yes, if the file name is fingerprinted from its content. Changing the content changes the URL, so the cached copy is never wrong. Without fingerprinting, a long lifetime is genuinely risky.
- Do ETags replace cache headers?
- No. An ETag saves the response body on revalidation; it does not save the request. For static assets, immutable avoids the round trip entirely, which is what matters on a slow connection.
Sources
Related
VeriFixScan crawls a site and applies its checks to every page it reaches, keeping the evidence behind each finding. Scanning one website is free.
Scan a website