Why isn't my CDN caching this?

← 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.

1. 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.

2. An accidental 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.

3. no-store, private, and no-cache are three different things

These are the most commonly confused directives in HTTP caching, and only two of the three stop a CDN from storing anything:

DirectiveBrowser cacheShared cache / CDN
no-storemust not storemust not store
privatemay storemust not store
no-cachemay store, must revalidate firstmay 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.

4. No explicit freshness lifetime at all

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.

5. 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.

6. A weak 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.

Reading the answer off one response

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.