---
title: Your CDN didn't go down. It stopped caching — and nothing told you.
description: Every other outage announces itself. A CDN that stops caching does the opposite: the site stays up, every status code stays 200, and your uptime dashboard stays green while every single request travels to your origin. Here's why that happens, why one request from one machine can never detect it, and what we built to catch it — including the claims we refused to make.
canonical: https://watchfor.io/blog/why-your-cdn-stopped-caching
---

[All posts](/blog) [Engineering](/blog/category/engineering) Sep 10, 2026 · 10 min read · WatchFor Team

# Your CDN didn't go down. It stopped caching — and nothing told you.

Every other outage announces itself. A CDN that stops caching does the opposite: the site stays up, every status code stays 200, and your uptime dashboard stays green while every single request travels to your origin. Here's why that happens, why one request from one machine can never detect it, and what we built to catch it — including the claims we refused to make.

There is one kind of production problem that no alert ever fires for.

Your site is up. Every check returns 200 . Response times look normal from
the office. Uptime is 100% for the month. And every single request a visitor
makes is travelling all the way to your origin server, because three weeks
ago someone added a Set-Cookie header to a response and your CDN quietly
decided it should stop caching that path.

You find out from the bandwidth bill. Or from your origin's CPU graph. Or
you never find out at all, and just live with a site that is three times
slower than it should be, paying for an edge network that is doing nothing
but forwarding traffic.

This post is about why that failure is invisible to ordinary monitoring,
what it actually takes to see it, and what we built. Some of it is about
the things we decided not to claim, which turned out to be the harder
half.

## A CDN makes exactly one promise

Strip away the marketing and a CDN promises one thing: the next person who
asks for this gets it from a machine near them, not from you.

Everything else — the DDoS protection, the TLS termination, the image
resizing — is a feature. The cache is the product. And unlike almost
everything else in your stack, when the cache stops working nothing
breaks . The response is still correct. The status code is still 200 .
The only difference is where the bytes came from, and that is not something
your users, your uptime monitor, or your error tracker can see.

So the question worth monitoring is not "is my CDN up". It is "is my CDN
still caching" — and those turn out to be very different questions.

## Why one request tells you nothing

Here is the naive check. Request the URL, read the cache header, done:

$ curl -sI https://example.com/static/app.js | grep -i cf-cache-status
cf-cache-status: MISS

A MISS . Is that a problem?

You cannot tell. There are two completely different worlds that produce
that exact response:

- The object is genuinely uncacheable, and every visitor is hitting your
origin. This is a real problem costing you real money.

- The object simply was not in that particular edge's cache at that
particular moment — because it had just been purged, or because nobody in
that city had asked for it in a while. This is a CDN working perfectly.

One request cannot distinguish these. And this is the part most CDN
checkers get wrong: they show you a MISS and let you draw a conclusion
that the data does not support.

The fix is embarrassingly simple once you see it. Ask again :

Request 1: MISS
Request 2: HIT
Request 3: HIT

That is an edge populating its cache and then serving from it — a CDN doing
exactly what you pay it for. Compare:

Request 1: MISS
Request 2: MISS
Request 3: MISS

Same first request. Completely different meaning. The object is not being
cached at all, and every visitor to that region is hitting your origin.

The sequence is the signal. A single reading is just a coin flip you have
mistaken for a measurement.

The claim we refuse to make. You will see tools describe the first
request as the "cold cache" run. We do not, because we cannot know it. We
have no idea whether that edge was holding your object before we asked —
it may have been serving it happily to thousands of people all morning.
What we can honestly report is what happened across our own requests, and
what it implies. Anything beyond that is a story, not a measurement.

## Why one location tells you nothing either

A CDN is not one thing. It is dozens of independent edges that can and do
disagree with each other.

This is not hypothetical, it is structural. Cache rules can be scoped by
region. Edges get purged independently. A tiered-cache setup gives some
regions a parent cache and others a direct path to your origin. A stale
object can sit in one PoP long after everywhere else has moved on. Any of
these produces a URL that is genuinely cached and fast where you happen to
be sitting, and genuinely uncached somewhere you are not — and from one
machine, the second half is invisible.

So the second requirement is uncomfortable but unavoidable: you have to ask
from many places at once. We use 20 probe locations across seven regions —
the Americas, Europe, the Middle East, Africa, Asia and Oceania — and the interesting findings almost always
come from the disagreement between them rather than from any single answer.

## The four reasons caching silently stops

If you are asking "why is my CDN not caching", four mechanisms account for
most of it — and all four are things the providers themselves document.

1. A Set-Cookie on the response. Many CDNs treat a Set-Cookie as a
signal that the response is user-specific and refuse to cache it — often
without being asked to. One analytics snippet, one A/B testing library, one
session middleware that got a little too eager, and a path that was cached
for a year is now uncacheable. This is the single most common cause we see.

2. A Vary header that fragments the cache key. Vary: Accept-Encoding
is fine and normal. Vary: Cookie is a disaster: it gives every distinct
cookie value its own cache entry, which in practice means every visitor gets
their own copy and the hit ratio approaches zero. Vary: * makes the
response uncacheable by any shared cache, full stop.

3. Cache-Control saying no. private , no-store , or max-age=0 .
Usually inherited from a framework default that was right for your HTML and
very wrong for your assets. Worth knowing: s-maxage overrides max-age
for shared caches specifically, so you can keep browsers conservative and
still let the edge hold things for a long time.

4. Query strings in the cache key. If the CDN includes the full query
string, then ?v=1 and ?v=2 are two different objects, and a cache-busting
parameter that changes per deploy — or worse, per request — guarantees a
permanent miss.

The useful thing is that all four are visible in the response itself . Read
what the response permits, then read what the edge actually did, and the gap
between the two is your answer. A response marked public, max-age=3600
that never becomes a HIT is a much more specific complaint than "caching
seems broken", and it points directly at the cache key.

## What we built

Our CDN checker used to do what most of them do: one HEAD request, match
the Server header against a list of keywords, print a provider name. It
answered a question you already knew the answer to.

So we rebuilt it. One URL in, and it now:

- requests it three times in a row from every one of our 20 locations ,
each on its own connection so the timings and the cache verdicts stay
independent;

- reads each provider's own cache vocabulary — Cloudflare's
cf-cache-status , Akamai's TCP_MEM_HIT , CloudFront's
Hit from cloudfront , Bunny's cdn-cache — and normalises them so
regions can actually be compared, while keeping the original wording next
to it;

- identifies the provider from five independent signals (the CNAME
chain, the network the edge address is announced from, provider-specific
headers, Server , Via ) and reports how confident it is and why ;

- names the edge that answered where the provider publishes one — the RIX
hidden in a Cloudflare ray ID, the FRA56 in a CloudFront POP header;

- hashes each response body after decompression , so a brotli region and
a gzip region serving identical content compare equal rather than looking
like a discrepancy;

- and compares every location's latency against the median of comparable
locations instead of a fixed threshold.

That last one deserves a sentence of its own, because it is what makes the
findings trustworthy. An absolute threshold — "warn above 200ms" — is wrong
in both directions. It nags a site that is honestly slow everywhere and has
no CDN problem to fix, and it stays completely silent when one region is
four times slower than its neighbours but all of them are under the limit.
Comparing each location against its own region is what makes a real anomaly
visible without inventing problems.

## What it looks like

Two real runs, taken while writing this.

First, a CDN doing its job — the jQuery bundle on cdnjs.cloudflare.com :

A+ 100/100 Cloudflare · high confidence
20/20 locations · 100% cache hits · TTFB p50 26ms · p95 31ms

Warsaw RIX HIT HIT HIT 30 ms
Frankfurt FRA HIT HIT HIT 28 ms
…
Cache-Control: no-transform, public, s-maxage=30672000, immutable
Verdict: cacheable
No problems found.

Now our own marketing site's homepage:

C Cloudflare · high confidence
100% of requests served from cache: no
DYNAMIC DYNAMIC DYNAMIC 155 ms
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Verdict: not-cacheable

Our own site scores a C . That is not a bug in the report — the HTML is
deliberately uncacheable because it is personalised, and DYNAMIC means
Cloudflare was told not to cache it and correctly did not. The report says
so rather than pretending otherwise, and the assets on the same site score
A+ . Which is exactly the lesson: check the URL you care about, not the
hostname. Caching is configured per path far more often than per host, and
a check on your apex tells you almost nothing about your JavaScript bundle.

## The grade, and why every point is explainable

The report gives a letter, A+ down to F. We were careful here, because a
single mysterious score is how a tool stops being useful — you cannot argue
with it, so you stop believing it.

So the grade is six weighted categories: availability (25%), cache (25%),
latency (20%), consistency (15%), edge health (10%) and protocols (5%). Each
one shows its own score and, if it is not 100, the exact sentences that took
points off:

Latency: 84
Mumbai was 3.8× its regional median TTFB (−16).

Two rules keep it honest. Any location that cannot load the URL at all caps
the grade at C , however tidy everything else is — a green A over a site
that half the world cannot reach would be worse than no grade. And a
provider that publishes no cache header at all scores 100 for cache
rather than 0: we have no evidence of a problem, and inventing a penalty out
of silence would punish an entire class of CDNs for their header style.

## The bugs our own report caught

Two worth admitting, because both were us making a claim we had not earned.

The first version told cdnjs.cloudflare.com that IPv6 was not
reachable . It has had AAAA records for years. What actually happened is
that the probe attempting the IPv6 request was on an IPv4-only network, so
it failed — and we reported our own gap as the customer's fault.

The rule now: the only IPv6 claim we are entitled to make is that a hostname
publishes no AAAA record beside a successful A lookup, because that is a
fact about their DNS that we read directly. A failed IPv6 request from our
side says as much about our vantage point as theirs, and is reported as
"not tested" . Throughout the report, null means we did not establish
it and false means we did — and those are never collapsed together.

The second: the old detector matched any Server header containing the
words "cdn", "google" or "amazon". A plain nginx origin behind a hostname
like cdn.example.com was confidently reported as being behind a CDN. It
had been wrong for a long time, quietly, in the direction people wanted to
believe.

## From "check it once" to "tell me when it changes"

A one-off check tells you where you stand today. What you actually want is
to be told when that changes, because it will — the next deploy, the

…

---

Canonical page: https://watchfor.io/blog/why-your-cdn-stopped-caching · Site guide: https://watchfor.io/llms.txt
