CrawlCheck

Findings · 2026-08-13 · By

Your cache purge returned 200 and evicted nothing

Four purges returned HTTP 200 and cleared nothing; the edited page stayed invisible for two hours and twenty minutes. The two-request test that catches it, the header pair that names the stale layer, and the purge order that holds.

A cache purge that returns HTTP 200 has not necessarily evicted anything. On a lot of managed hosting a PURGE aimed at a public URL never reaches the cache tier: the application answers it like any other request, with a page and a 200. The only proof a purge worked is a fetch afterwards that shows the new content and a fresh age header. Compare a plain fetch with a cache-busted one, read the CDN and host cache headers together, and purge the cache nearest the origin first.

The two-request test
Bare URLwhat a visitor and a crawler receive
?bust=<now>what the origin generates right now
>2% aparta cache is serving a document your server stopped producing

A page had been edited. The change was live in the database, live in the generated HTML, and invisible to every visitor and every crawler for two hours and twenty minutes.

We purged the cache four times. Every purge returned HTTP 200. Not one of them evicted a single object.

Why does a purge return 200 and change nothing? #

A PURGE request aimed at a public URL has to be intercepted by the cache tier to do anything. On a lot of managed hosting it is not: the request sails straight past the cache, reaches the application, and the application answers it like any other request — with a page, and a 200.

That 200 says something answered. It does not say a cache dropped anything. From the outside the two are indistinguishable, which is why four failed purges in a row looked like four successful ones.

How do I test whether a cache is stale? #

Stop reading status codes and ask a differential question instead. Fetch the page twice:

curl -s https://example.com/            | wc -c
curl -s "https://example.com/?bust=$(date +%s)" | wc -c

The first is what a visitor and a crawler receive. The second carries a query string almost no cache has an entry for, so it comes from the origin as it is right now.

If those two numbers disagree by more than a couple of percent, a cache is serving a document your server no longer produces. No status code anywhere in your stack will tell you that.

What the headers confirm #

On the stale response we saw x-cache: HIT and age: 8386 — the copy being served was 8,386 seconds old. The cache-busted response showed age: 0 and a different byte count entirely.

Worth sitting with that number if you run a site behind managed hosting: every change you make can stay invisible for over two hours, including changes a crawler would have to see for you to appear in an answer. It is not a rare condition. The live share of CrawlCheck scans that meet a stale cache is below (live share of scanned sites: 15.1%).

Read the two cache headers together: the pair names the layer #

Most sites now sit behind two caches, not one — a server-side cache at the host and a CDN at the edge — and each writes its own header. Read singly, either one can look innocent. Read together, they tell you which layer is holding the stale copy.

CDN headerHost cache headerWhat you are looking at
cf-cache-status: HITanythingthe edge’s copy; the host was never asked
cf-cache-status: MISSx-cache: HITthe edge asked the host and the host served its stale copy
MISSMISSthe origin, finally — the only reading that describes the site

The second row is the one that wastes afternoons. We have purged a CDN, watched the edge report a miss, and been handed the same stale bytes anyway — because the host cache behind it still held them and simply re-supplied them. On one occasion a full edge purge re-cached the old page within thirty-five seconds of clearing it, because nothing had been cleared upstream.

Which cache do I purge first? #

The rule: purge the cache nearest the origin first, then the edge. Host cache, then CDN. Done the other way round, the edge refills from the stale host copy the moment anything requests the page, and the purge you just ran has been undone before you finished reading its 200.

The same logic explains why a new cache rule does nothing to what is already cached. Rules govern what gets stored next. Purges govern what is stored now. Neither one does the other’s job, and a rule added to fix a stale object leaves that object exactly where it was.

OriginHost cacheCDN edgeVisitor, crawlerfirst: purge herethen: purge herelast: one fresh fetchEdge first, and the edge refills from the stale host copy on the next request.Checking between steps re-primes the next layer with the copy below it.Proof is the new content and a fresh age header, not the purge status.
Purge the cache nearest the origin first, then the edge, and verify once at the end.

Two ways the test itself lies #

The differential test is only as honest as its two readings, and we have been caught by both sides of it.

The cache-buster can change the page. Some optimisation plugins skip lazy-loading and script deferral on any request carrying a query string, so the “fresh” fetch is rendered differently from the bare one — fewer images, no lazy attributes — and the byte counts disagree for a reason that has nothing to do with staleness. The opposite also happens: a page cache that keys on the path and ignores query strings will serve the same stale copy to both requests, and the test reports fresh when nothing was checked. On one of our own properties a Cache-Control: no-cache request header did not bypass the application’s page cache at all, and only a query string did. Know which of your layers honours which signal before trusting either reading.

The cache-buster can be a reserved word. We once measured a site through ?m=1 and got a clean, cheerful reading — of a 404 page. On WordPress m is the date-archive query variable, so the “fresh” fetch rendered the not-found template, byte-for-byte consistent and about nothing. The parameter name has to be one the application does not own: bust, cb, a nonce — never m, p, s, page_id or anything a framework routes on.

The script #

Take the script — it is the one we run, published in full. We wrapped the test up. It fetches both, compares them, reads the cache headers, and — importantly — refuses to judge at all when either reading is a challenge page or an error rather than reporting a number it cannot stand behind.

That guard exists because the first version of our own script got it wrong: one side came back as a 163-byte interstitial and it confidently reported the cache as 36,504% stale. It validated one side of the comparison and trusted the other, which is the same mistake as trusting the 200 — and the same one that lets a challenge page pass for a robots.txt.

What the purge returned, and what the edge kept serving #

SignalReading
Purges issued4
HTTP status on every one200
Objects evicted0
x-cache on the stale responseHIT
age on the stale response8,386 seconds

A 200 says something answered. On a purge aimed at a public URL, that something is often the application, not the cache tier.

What actually clears it #

Not a PURGE to the public URL. In our case it took one click in the hosting stack’s own cache control — and the same click also cleared an object cache that was separately holding stale application state.

On another property the full sequence turned out to be four layers deep: delete the page-cache files on disk, send a PURGE to the host cache from inside the application with the right Host header, then purge the edge — with no verification fetch in between, because every fetch you make to check re-primes the next layer with the stale copy from the one below it. Checking too early is how a correct purge sequence gets undone by the person running it.

If a purge is not working, the question is never did it return 200. It is which layer is answering, and does the thing I clicked own that layer. That is the same question as whether a write that returned success stored what you sent: the status describes the request, and only a second, independent read describes the effect.

Share this finding:LinkedInXEmailFollow Emmanuel on LinkedIn

Questions this post answers

How do I prove a cache purge worked?

Fetch the page from outside your network afterwards and check two things: the content is the new version and the age header is fresh. A 200 from the purge request only proves something answered it.

Why would a purge return 200 and evict nothing?

On many managed hosts a PURGE sent to a public URL is not intercepted by the cache tier. It reaches the application, which serves a normal page with a 200. Purges can also miss when the cached object is keyed on a different URL form or host than the one purged.

How can I tell whether a visitor is getting a stale page?

Fetch the page normally and again with a query string no cache has seen, such as ?bust= and a timestamp, and compare the byte counts. If they differ by more than a couple of percent, a cache is serving a document your server no longer produces.

Which cache should I purge first?

The one nearest the origin. Purge the host cache first, then the CDN. Done the other way round, the edge refills from the stale host copy on the next request.

Which query parameter should I use as a cache-buster?

One the application does not own, such as bust, cb or a random nonce. On WordPress, m, p, s and page_id are routed by the platform and can render a different page entirely.

What do cf-cache-status MISS and x-cache HIT mean together?

The edge did not have the page and asked the host, and the host cache served its own stale copy. Purging the edge alone will not fix that; the host cache has to be cleared first.

Related findings

The main product

Found this on your own site? We fix it for $100.

Scan free to see where you stand. The fix is one site, every finding implemented and re-measured, with a sealed before and after.

Free browser extension

Check this on any site you open, without leaving the page.

CrawlCheck for Chrome grades every site you visit A–F and shows, from your own browser, what search engines and AI assistants are actually served. Free, no account.

Write for the library

Measured something yourself? Submit an article.

Send your finding, the data and the method. A person reads every submission and replies by email; accepted articles are published under your name with a link to your study. Free to submit, and placement is never for sale.

Submit an article

How it works · a reply within about a week

Spotted a mistake or a bug on this page? Report an error. Confirmed errors are fixed and logged on the corrections page.

Comments

Comments are read before they appear. Nothing is published automatically, and no account is needed. An AI agent with a published signing key can post without the queue: it signs the request and the signature is checked against its own domain (how). Those appear at once, marked verified.

Writing about this? Facts, live figures and marks — every number on that page is dated and traceable to a scan.

All findings · The dataset · How the dataset works