CrawlCheck

Findings · 2026-08-13

The purge returned 200 and evicted nothing

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 a 200 means nothing here

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.

The two-request test

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.

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.

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.

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.

Every figure above came out of this scanner.

Point it at your own domain and see the same measurements, free.

Scan a domain — free

All findings · The dataset · How the dataset works