← Back to Cache-Control Checker
A response your CDN stores nothing for is almost always one of six things, and they are worth checking in this order — the first one outranks everything below it, so finding it second wastes the time you spent on the others. Every rule here is a protocol requirement with a citation, not a tuning preference: each one is something a compliant cache must do, which is why changing the CDN's settings will not work around it.
All six read off the response headers the origin actually sent. To see yours:
node check.mjs https://example.com/your-asset.js --json
…then paste the JSON into the report viewer, which names which of these patterns it found and quotes the header that proves each one. Only point it at a system you own or are authorised to test.
Vary: * — never reusable, whatever else you set
RFC 9111 §4.1 states
that a Vary field value of * always fails to match. A cache may
store the response, but it can never select it for reuse against any later request, however
identical. This outranks every other caching header in the response:
Cache-Control: public, max-age=31536000
Vary: *
is never served from cache, by any compliant browser or CDN, ever. It is worth checking first for two reasons: no setting on the CDN side can override it, and it is nearly always accidental — copied from a template, or added defensively against a content-negotiation bug that has since been fixed, and then never removed. If you have one header to look at, look at this one.
Set-Cookie
Most CDNs decline to cache a response carrying Set-Cookie by default, and the reason
is sound: storing it would mean handing one visitor's cookie to the next one served from that copy.
The usual cause is a session framework configured to start a session on every request,
including requests for static assets that nobody intended to make uncacheable.
The inverse case is the dangerous one, and it is the reason this checker rates it a danger rather
than a notice. If the response says Cache-Control: public and sets a cookie,
it is explicitly inviting every shared cache between the origin and the visitor to store and reuse
it. A shared cache that is not specifically configured to strip Set-Cookie on write can
then serve one visitor's session cookie to a different visitor. "The CDN won't cache this" is the
benign symptom of that pairing; a cache that does cache it is the incident.
no-store, private, and no-cache are three different thingsThese are the most commonly confused directives in HTTP caching, and only two of the three stop a CDN from storing anything:
| Directive | Browser cache | Shared cache / CDN |
|---|---|---|
no-store | must not store | must not store |
private | may store | must not store |
no-cache | may store, must revalidate first | may store, must revalidate first |
no-cache is the one that catches people out: it does not mean "do not cache".
The CDN still holds the bytes and still answers a revalidation with a 304, so it is still saving
origin bandwidth. Concluding that a no-cache response is uncacheable — and then going
looking for a CDN bug — is a frequent dead end.
no-store alongside a max-age is a contradiction rather than a compromise:
the max-age is dead text, because nothing was stored for it to apply to. This checker
reports that pairing as a notice, since it usually means two pieces of configuration are fighting
and one of them is silently losing.
A response with no max-age, no s-maxage and no Expires has no
explicit freshness lifetime. RFC 9111 permits a cache to apply a heuristic lifetime in that
case, but heuristics differ between implementations and plenty of CDNs default to not caching
rather than guessing on your behalf. Relying on that default is relying on undocumented behaviour
that can change under you.
The clearest version is a static-asset-shaped path — a content-hashed filename, a
.js or .css extension — served with no caching headers whatsoever. That is
almost never a decision; it is an origin that was never configured for those paths. A hashed
filename is specifically designed to be safe to cache for a year, so a missing header there is pure
loss.
Expires that is overridden, or already stale on arrival
Two distinct failures share one header. First, if both Expires and
Cache-Control: max-age are present, RFC 9111 requires the cache to ignore
Expires entirely. A long Expires sitting beside a short
max-age does nothing at all — the lifetime you think you granted is not the one in
effect.
Cache-Control: max-age=60
Expires: Thu, 31 Dec 2026 23:59:59 GMT <-- ignored; the lifetime is 60 seconds
Second, an Expires value at or before the response's own Date header means
the response is born already stale. That is usually a hardcoded past date left over from being used
as a cache-buster, which then keeps busting long after the reason for it went away.
ETag on a range-capable response
This one is not a caching failure and will not show up as a cache miss — it is here because it is
the caching-adjacent bug people find last and pay for most.
RFC 9110 §13.1.5
requires a strong validator for a conditional range request. A response that
advertises Accept-Ranges: bytes but returns a weak ETag:
Accept-Ranges: bytes
ETag: W/"abc123"
cannot safely support If-Range. A resumed download, or a client fetching segments in
parallel, can end up assembling bytes that belong to two different versions of the resource. The
symptom is a corrupt large file — not a slow one — which is why it is almost never found by someone
investigating a caching problem, and why this tool reports it alongside them.
Every check above is decidable from a single response's headers, which is what the Cache-Control Checker does: one real GET, then a report naming each pattern it matched and quoting the exact header that proves it. It deliberately does not grade your caching strategy or guess at a target hit rate — those depend on your traffic, not on the protocol — and it does not crawl anything beyond the one URL you name.
Related response-header evidence on the same site:
Security Headers checker for Content-Security-Policy and
clickjacking headers, and Cookie Security checker for
Set-Cookie attribute patterns — a different class of finding than the caching headers
here, though step 2 above is where the two overlap.