Agent view · Findings · 2026-08-13
Your cache purge returned 200 and evicted nothing
This is the response this server returns to a client that asks for Accept: text/markdown at https://crawlcheck.io/blog/the-purge-returned-200-and-evicted-nothing: fetched when you opened this page, shown byte for byte. An assistant, a crawler that negotiates markdown, or anything reading our feed gets exactly this. The markdown leaves signed (RFC 9421, key in our key directory), so a crawler can prove the page it read is the page this server sent; the check is shown below.
| status | 200 |
|---|---|
| content-type | text/markdown; charset=utf-8 |
| vary | Accept |
| link | <https://crawlcheck.io/llms.txt>; rel="describedby"; type="text/plain", <https://crawlcheck.io/.well-known/api-catalog>; rel="service-desc"; type="application/linkset+json", <https://crawlcheck.io/blog/the-purge-returned-200-and-evicted-nothing>; rel="cite-as", <https://crawlcheck.io/.well-known/http-message-signatures-directory>; rel="http-message-signatures-directory" |
| cache-control | public, max-age=900, stale-while-revalidate=2400 |
| bytes | 17,161 |
| words | 2,640 |
| sha-256 | ba7ca6ce803aeb366b0dab4a8b629c1e433361ce41543dd74c2355adbe643218 |
| repr-digest | sha-256=:unymzoA66zZrDatKi2KcHkMzYc5BVD3XTCNVrb5kMhg=: ← matches the body shown |
| signature-input | sig1=("@authority";req "@path";req "repr-digest" "content-type");created=1794450832;expires=1794454432;keyid="1hHmE3fo1rg5Ig70wuFtJ5wBc7G-sUTgUJkxyPWFRps";alg="ed25519";nonce="ioSf5ODOwuAQD58WNH-ajA";tag="crawlcheck-page" |
| signature | sig1=:Awn3nb07lymOm7Tcvj5c5jFD4YiEueC5cikybc/CeHbT76IG85+MD4PH8ts+Dpn5nCHW1NT1JezluAnThVjKAQ==: |
| verified | yes — signature by key 1hHmE3fo1rg5Ig70… from https://crawlcheck.io/.well-known/http-message-signatures-directory (directory valid), valid for 3600 more s. Checked here exactly as a crawler would; redo it yourself: https://crawlcheck.io/api/tool/wba |
Findings · 2026-08-13 · By [VSNARY | Emmanuel Orta](/author) # 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. On this page - Why does a purge return 200 and change nothing? - How do I test whether a cache is stale? - What the headers confirm - Read the two cache headers together: the pair names the layer - Which cache do I purge first? - Two ways the test itself lies - The script - What the purge returned, and what the edge kept serving - What actually clears it The two-request test **Bare URL**what a visitor and a crawler receive **?bust=`<now>`**what the origin generates right now **>2% apart**a 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](/glossary/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%**). ## 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](/glossary/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 header | Host cache header | What you are looking at | | --- | --- | --- | | `cf-cache-status: HIT` | anything | the edge’s copy; the host was never asked | | `cf-cache-status: MISS` | `x-cache: HIT` | the edge asked the host and the **host** served its stale copy | | MISS | MISS | the 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. 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](/glossary/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](/tools/cachecheck.sh)** — 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](/blog/a-file-that-answered-200-and-could-not-be-read). ## What the purge returned, and what the edge kept serving # | Signal | Reading | | --- | --- | | Purges issued | 4 | | HTTP status on every one | 200 | | Objects evicted | 0 | | x-cache on the stale response | HIT | | age on the stale response | 8,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](/blog/a-write-that-returned-success-and-corrupted-every-newline): the status describes the request, and only a second, independent read describes the effect. Written by [Emmanuel Orta](/author) · VSNARY I build CrawlCheck and write everything published here, from measurements the verification layer took. Where a post got something wrong, the correction stays in the post. [Every post, and where I got it wrong →](/author) · [Follow on LinkedIn →](https://www.linkedin.com/in/emmanuelorta) Share this finding: [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcrawlcheck.io%2Fblog%2Fthe-purge-returned-200-and-evicted-nothing%3Futm_source%3Dlinkedin%26utm_medium%3Dsocial%26utm_campaign%3Dshare)[X](https://twitter.com/intent/tweet?url=https%3A%2F%2Fcrawlcheck.io%2Fblog%2Fthe-purge-returned-200-and-evicted-nothing%3Futm_source%3Dx%26utm_medium%3Dsocial%26utm_campaign%3Dshare&text=Your%20cache%20purge%20returned%20200%20and%20evicted%20nothing)[Email](mailto:?subject=Your%20cache%20purge%20returned%20200%20and%20evicted%20nothing&body=https%3A%2F%2Fcrawlcheck.io%2Fblog%2Fthe-purge-returned-200-and-evicted-nothing)[Follow Emmanuel on LinkedIn](https://www.linkedin.com/in/emmanuelorta) ## 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 [2026-09-30 Stale cached robots.txt or llms.txt? Two fetches tell you CDNs cache robots.txt and llms.txt hardest. Two fetches, bare and cache-busted, show when edge and origin disagree, and why a new](/guides/machine-file-caching-stale-split-and-blocked-behind-the-edge)[2026-08-28 How to check if AI crawlers can read your site Five curl checks that show whether GPTBot, ClaudeBot and PerplexityBot can read your site: content type, cache, crawler vs browser](/guides/how-to-check-if-ai-crawlers-can-read-your-site)[2026-08-13 robots.txt returns 200 but no crawler can read it A robots.txt or llms.txt that answers 200 with a challenge or login page reads as no file. Why monitors miss it, and the two-reque](/blog/a-file-that-answered-200-and-could-not-be-read) 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. [Fix it all — $100](https://buy.stripe.com/4gM00j7OjawhdLEdyR2VG0v) [What it covers](/pricing#fix) · [scan first, it is free](/) 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. [Add to Chrome — free](https://chromewebstore.google.com/detail/plgjengobhbdmbgckiiaaflgiepdbcpi?utm_source=crawlcheck.io&utm_medium=post) [What it does](/extension) 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](/submit-finding#send) [How it works](/submit-finding) · a reply within about a week Spotted a mistake or a bug on this page? [Report an error](/report-error?k=article). Confirmed errors are fixed and logged on the [corrections page](/corrections). ## 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](/api/comment/signed)). Those appear at once, marked *verified*. Send for review Writing about this? [Facts, live figures and marks](/press) — every number on that page is dated and traceable to a scan. [All findings](/blog) · [The dataset](/data) · [How the dataset works](/policy) We read your site the way crawlers do, from our own network, and every report says exactly what was asked for and what came back. [Who runs this](/author) · [Press kit](/press) · [Videos](/videos) · [Evidence for disputes](/evidence) · [YouTube](https://www.youtube.com/@crawlcheck) · [hello@crawlcheck.io](mailto:hello@crawlcheck.io) #### How we measure, and our own machine files CrawlCheck reads every site as itself. Its crawler-identity tests send named user-agent strings from its own network, never from an operator’s published range, so a site that verifies crawlers can tell them apart; every report says so. Results reflect what an anonymous non-browser client receives. [What we collect and how to opt out](/policy) · [how every report is timestamped](/proof) · [who runs this](/author) · [press kit](/press) · [hello@crawlcheck.io](mailto:hello@crawlcheck.io). **Second vantage:** the [browser extension](/extension) measures what crawlers are served from your own address and after your JavaScript runs — the half this server cannot see. From 1.0.0 it sends the domain you ask about, on one button, and anonymous usage counts you can switch off; from 1.0.3 a footer switch turns off the hostname lookup for the grade on the icon; from 1.0.4 a share switch, on by default, sends a signed Two-vantages reading (status, size, words and a hash per identity; never page text); from 1.0.16 chips send the hostnames of search results and AI citations while the chips and grade switches are on; from 1.0.17 a readings switch, on by default, sends one signed page reading a day (numbers, the hostname and the page address with the query removed; never in incognito; at most 40 a day) and only while the share and grade switches are also on. Earlier versions send nothing. **Our own machine layer for the agentic web:** [llms.txt](/llms.txt) · [llms-full.txt](/llms-full.txt) · [agents.md](/agents.md) · [SKILL.md](/SKILL.md) · [entitymap.json](/entitymap.json) · [media kit](/.well-known/media-kit.json) · [MCP server card](/.well-known/mcp/server-card.json) · [API catalog](/.well-known/api-catalog) · [OpenAPI](/openapi.json) · [robots.txt](/robots.txt) · [sitemap.xml](/sitemap.xml) · [security.txt](/.well-known/security.txt) — every one named in robots.txt, which is exactly what we check for on yours. **Pages:** [Entity map validator](/entity-map-validator) · [Status](/status) · [SDK](/sdk) · [Certificates](/certificates) | **Glossary:** [Heading hierarchy](/glossary/heading-hierarchy) · [MCP server](/glossary/mcp-server) · [Red teaming](/glossary/red-teaming) · [verdict over DNS](/glossary/verdict-over-dns) · [nosnippet](/glossary/nosnippet) | **Directory:** [Category: Inference compute](/directory/category/inference-compute) | **Registry:** [bolt.new](/registry/bolt.new) · [deepmind.google](/registry/deepmind.google) [Contact](/contact) · [Partnerships](/partnerships) · [Terms of service](/terms) · [Refund policy](/refunds) · [Trust center](/trust) · [Report an error](/report-error)
Same body as a file: /blog/the-purge-returned-200-and-evicted-nothing.md · as data: /feed.json · what we tell agents site-wide: /llms.txt, /agents.md. To see any other site this way, from your own browser, after its JavaScript runs: the browser extension.