Two caches, one purge button

There are at least two caches between your server and the visitor's screen, and the purge button in the Cloudflare dashboard only reaches one of them.

  • The edge cache lives in Cloudflare data centers. You control it entirely: you can purge it, set its lifetime with an Edge TTL, and inspect its decisions through the cf-cache-status response header.
  • The browser cache lives on each visitor's machine. Its lifetime is set by the Cache-Control header you sent when the file was first downloaded, and once that header is out, you cannot take it back. Cloudflare's own documentation is explicit on the point: purging the Cloudflare cache does not affect assets stored by a visitor's browser.

This single distinction explains the large majority of "I purged and nothing changed" reports. If you served a stylesheet with Cache-Control: public, max-age=31536000 yesterday, your browser will keep using its local copy for a year, no matter how many times you purge the edge. The purge worked, but the copy you are looking at comes from the browser, not from the edge.

Two practical consequences. First, always confirm a purge with a tool that has no cache of its own, curl being the obvious candidate, before changing anything else. Second, be conservative with Browser TTL: Cloudflare's default browser cache TTL is four hours. With this setting, a browser may still display the old version for several hours after deployment.

What a purge actually does, and which one to use

Cloudflare offers five purge methods, and since April 2025 all of them are available on every plan, Free included. Only the rate limits differ between plans.

  • Purge by single file (URL): the method Cloudflare recommends. It deletes one object, and the next request for it returns MISS. Up to 100 URLs per request on Free, Pro and Business.
  • Purge by prefix: clears everything under a path, for example example.com/blog. It ignores query strings, which is exactly why it rescues purges that fail by URL. Maximum 100 prefixes per request, and no more than 31 path separators per prefix.
  • Purge by tag: relies on a Cache-Tag header your origin adds to responses. The aggregate header cannot exceed 16 KB, roughly a thousand tags. This is the right tool for a CMS that needs to invalidate one article across dozens of URLs.
  • Purge by hostname: clears everything served under a given host, up to 100 hostnames per request.
  • Purge Everything: clears the entire zone. Cloudflare explicitly advises against using it as a routine, because every subsequent request goes back to your origin at once. On a busy site that is a self-inflicted traffic spike, and the symptoms can look like intermittent 521, 522 or 524 origin errors.

Rate limits are worth knowing before you conclude that a purge "did not fire". For hostname, tag, prefix and Purge Everything, the limits are per account: five requests per minute on Free, five per second on Pro, ten per second on Business, fifty per second on Enterprise. A deployment script that purges in a loop on a Free plan will silently hit that ceiling.

Why purging by URL so often misses

A single-file purge does not target a file, it targets a cache key. The default cache key is the full URL: scheme, host, and the URI including its query string. Everything below follows from that.

  • The query string counts. Purging https://example.com/style.css does not purge https://example.com/style.css?v=3. They are two distinct objects at the edge. Cache-busting parameters, the very technique used to force updates, are also what makes purge by URL miss.
  • Scheme and host must match exactly. A purge submitted for http:// does not touch the https:// object, and example.com is not www.example.com.
  • The path is case-sensitive, the host is not. Cloudflare lowercases the host part according to RFC rules, but /HelloHi and /hellohi remain two different keys.
  • A custom cache key breaks dashboard purges. If a cache rule builds the key from headers or cookies, the dashboard cannot send those values, so the purge silently fails to match. Use the API and include the header and cookie values, or fall back to prefix, tag or Purge Everything. Custom keys that only change query string handling generally still work from the dashboard.
  • A cache rule that only matches GET blocks the purge. Purge requests do not arrive as GET. A rule written as (http.host eq "example.com" and http.request.method eq "GET") will not match during a purge. Cloudflare's own fix is to widen the expression: (http.request.method eq "GET" or http.request.method eq "PURGE"). This one is invisible from the dashboard and costs hours if you do not know it.
  • Transform rules invert the URL to use. If a transform rule rewrites part of the path, a single-file purge must use the original end-user URL, while a prefix purge must use the post-transform origin URL. Getting this backwards produces a purge that reports success and changes nothing.

How long a purge really takes

Far less time than people assume. Cloudflare reports its purge system performing consistently under 150 ms for tags, hostnames and prefixes, and around 250 ms at the median for single-file purge. A purge that has produced no effect after a minute should be treated as having failed.

One legitimate exception: with Tiered Cache enabled, a prefix purge can return EXPIRED rather than MISS on the next request, because the lower tier revalidates against the upper tier instead of going straight to your origin. That is the system working as designed, not a failed purge.

If content is still stale ten minutes later, waiting longer will not help; narrow the field of causes: browser cache, wrong cache key, or a purge request that never actually ran.

When a cache rule looks like it does nothing

Cache rules fail quietly. They do not warn you that they never matched, and the dashboard shows no counter of how often a given rule fired. Four causes account for nearly all of it.

  • The last matching rule wins, not the first. This is the most expensive misunderstanding on the list, because it is the opposite of how legacy Page Rules behaved. Cache rules stack: several rules can match the same request and all of them apply, and where two rules set the same option, the value in the last matching rule is the one used. A permissive bypass rule sitting at the bottom of your list will quietly overrule the careful rule you just wrote at the top.
  • The expression does not match what you are testing. Rules match on the request, so a rule keyed on http.host, on a file extension, or on a path prefix will skip the URL you are curling if any of those differ. Test the exact URL targeted by the rule, with the same hostname and path.
  • Origin headers take the decision back. Edge TTL has three modes, and two of them hand control to your origin. "Use cache-control header if present, use default Cloudflare caching behavior if not" and "Use cache-control header if present, bypass cache if not" both defer to the response header. Only "Ignore cache-control header and use this TTL" overrides it. If your origin sends no-store or private and your rule respects origin headers, the rule is applied and the content is still not cached. Note also that Cloudflare reads Cloudflare-CDN-Cache-Control first, then CDN-Cache-Control, then Cache-Control: a CDN-Cache-Control: no-store beats a perfectly good Cache-Control: public, max-age=3600.
  • Edge TTL and Browser TTL are not the same setting. Edge TTL governs how long Cloudflare keeps the object; Browser TTL governs the Cache-Control value sent to the visitor. Changing one leaves the other alone. Note that the minimum Edge TTL depends on the plan: two hours on Free, one hour on Pro, one second on Business and Enterprise. On a Free plan you cannot set a five-minute edge cache, which surprises people testing short TTLs.

One more thing that is easy to misread: a cache rule set to Bypass cache usually produces DYNAMIC in the response header rather than BYPASS, because the rule makes the content ineligible for cache before the origin response is even examined. In this case, DYNAMIC is expected: the rule has correctly excluded the resource from caching.

Reading cf-cache-status without guessing

Everything above is confirmed or ruled out by one response header. Here is what each value actually means, using Cloudflare's own definitions.

  • HIT: the object was found in the edge cache and served from there. Your origin was not contacted.
  • MISS: the object was eligible for caching but was not in the cache, so it came from your origin. A single MISS right after a purge is the expected, correct result.
  • EXPIRED: the object was in the cache but past its TTL, so it was fetched from the origin again. Frequent EXPIRED means your TTL is shorter than your traffic pattern needs.
  • REVALIDATED: the object was in the cache, Cloudflare asked your origin whether it had changed, and the origin said no. This is the one most often mistaken for a healthy cache. The body is served from the edge, so you save bandwidth, but a request still travels to your origin and back on every single hit, so latency and origin request volume barely move. It appears when stale-while-revalidate is absent, or when must-revalidate or no-cache forbid serving stale content.
  • UPDATING: the object was expired but served from cache anyway while the origin refreshes it in the background. This is the expected status with asynchronous revalidation, and it is a good sign.
  • STALE: served from cache past its expiry because Cloudflare could not reach your origin. A cache status that is really an origin availability alert.
  • BYPASS: the object was eligible for cache at request time, but the origin response turned out not to be cacheable. no-store, a bare private, a Set-Cookie header, Vary: *, or a body larger than the plan's cacheable size limit all land here. The decision was made at response time.
  • DYNAMIC: Cloudflare decided at request time that the asset was not eligible for cache and went to the origin without even looking in the cache. Most of the time this is not a fault: HTML and JSON are not in the default cacheable extension list, so an uncached HTML page returning DYNAMIC is standard behavior. It also appears when a bypass rule matched, or when Development Mode is on.
  • NONE/UNKNOWN: the response never reached the cache layer at all. A Worker answered without a subrequest, a security rule blocked the request, or a redirect rule answered first.

The Age header is a useful second opinion. Cloudflare sets it on HIT, STALE and UPDATING responses, and not on MISS, DYNAMIC, BYPASS, or on EXPIRED and REVALIDATED responses that revalidated against the origin. An Age value climbing across successive requests confirms a real cached object; no Age at all on a supposedly cached asset confirms the opposite.

Development Mode, often mistaken for a purge

Development Mode is a frequent source of false diagnoses in both directions. It suspends edge caching and Polish for three hours unless you switch it off earlier, and it does not purge anything: existing cached objects stay exactly where they are, they are simply not consulted while the mode is on.

The confusion goes like this. You turn Development Mode on, see your changes instantly, conclude the problem is solved, and forget about it. Three hours later the mode expires on its own, the edge starts serving its stored objects again, and the old content comes back without anyone having touched a setting. What you are seeing is not a failing purge but the end of the bypass. While it is active, expect cf-cache-status: DYNAMIC on everything, which is also why a zone under Development Mode looks like a cache that has stopped functioning.

If you need a bypass longer than three hours, Cloudflare's own recommendation is a cache rule with the Bypass cache setting, not a repeatedly renewed Development Mode.

A worked example: this very site

cfgarage.com is hosted on Cloudflare Pages, and it illustrates the REVALIDATED trap nicely. Requesting the site's stylesheet twice in a row returns MISS then REVALIDATED, never HIT, because the response carries cache-control: public, max-age=14400, must-revalidate. The must-revalidate directive is doing exactly what it says: once the object is considered stale it may not be served without checking with the origin first, which rules out the stale-while-revalidate path that would have produced HIT or UPDATING.

The HTML pages behave differently again: they are served with cache-control: public, max-age=0, must-revalidate and return DYNAMIC, since HTML is not a default cacheable extension and no cache rule declares it eligible.

Seen quickly, this zone looks like a cache that does nothing. Interpreted correctly, every status follows from a deliberate header. The same reasoning applies beyond this site: before deciding the cache is broken, check what your own platform is asking the cache to do. Your hosting provider, Cloudflare Pages, or your site's framework may set these headers automatically. Check the values actually being sent, even if you have not configured any cache settings yourself. Our companion guide on a low Cloudflare cache hit ratio covers the other half of the problem, when content should be cached and simply is not.

Step by step diagnosis

Run these in order. Each step eliminates one suspect, so you always know what the next result means. More field-tested checklists are collected in our Cloudflare guides hub.

  1. Take the browser out of the equation. Ask for the exact URL twice and compare:
    curl -sI https://www.example.com/style.css | grep -iE "cf-cache-status|cache-control|age|cf-ray"
    curl -sI https://www.example.com/style.css | grep -i "cf-cache-status"
    If curl shows the new content or a MISS followed by a HIT while your browser still shows the old page, the edge is fine and the browser cache is your only remaining problem.
  2. Check whether the URL you purged is the URL you are testing. Include the query string, the scheme and the exact hostname:
    curl -sI "https://www.example.com/style.css?v=3" | grep -i "cf-cache-status"
    If the bare URL returns MISS and the versioned one returns HIT, you have found a cache key mismatch and purge by prefix is your answer.
  3. Confirm the purge was accepted. Purge one URL from Caching > Configuration > Custom Purge, then immediately re-request it. The expected sequence is MISS on the first request and HIT on the second. Anything else means the purge did not reach the object you think it did.
  4. Read what your origin is really sending. Bypass the proxy and compare:
    curl -sI --resolve www.example.com:443:203.0.113.10 https://www.example.com/style.css | grep -iE "cache-control|cdn-cache-control|set-cookie|vary"
    Replace the address with your origin's. A no-store, a bare private, a Set-Cookie on a public asset or a Vary: * explains a BYPASS immediately, and a CDN-Cache-Control header will quietly outrank whatever Cache-Control says.
  5. Check Development Mode. Open Caching > Configuration. If the toggle is on, everything you are observing is a bypass, not a cache. Turn it off and retest before drawing any conclusion.
  6. Audit the cache rules, bottom to top. Open Caching > Cache Rules and read the list from the bottom, since the last matching rule wins on conflicting settings. Then check Rules > Page Rules for legacy entries, keeping in mind that cache rules take precedence over Page Rules by design.
  7. Inspect the rule that should be firing. Verify that its expression matches the URL you tested, that Edge TTL is not silently deferring to origin headers, and that the expression is not restricted to GET only, which would also prevent single-file purges from matching.
  8. Only then consider Purge Everything. Use it as a confirmation step, not a habit, and expect a burst of origin traffic straight afterwards. If the content is correct after a full purge but wrong again later, the problem is a rule or a header, not the cache.

Mistakes that keep the old version alive

  • Testing in the browser that caused the confusion. A hard reload does not clear everything, and service workers add a third cache the dashboard cannot see. Use curl or a private window as the reference.
  • Purging repeatedly instead of diagnosing once. A purge that did nothing the first time will do nothing the fifth, and on a Free plan five requests per minute is the ceiling anyway.
  • Setting a long Browser TTL on assets that change in place. A one-year browser cache is safe only on versioned filenames. On a file whose name never changes, it is a decision you cannot undo for a year.
  • Leaving Development Mode on as a workaround. It expires by itself after three hours, and the original problem reappears with no visible change to blame.
  • Assuming rule order behaves like Page Rules. Several Cache Rules can apply to the same request. If they define different values for the same setting, the last matching rule takes precedence. Check the rules further down the list as well.

Frequently asked questions

I ran Purge Everything and the Cloudflare cache is still not clearing. Why?

In most cases the edge cache did clear and your browser is serving its own copy. A purge removes objects from Cloudflare data centers only; it has no reach into the caches stored on your visitors' machines. Cloudflare states plainly that purging its cache does not affect assets stored by a visitor's browser. Retest with curl or in a private window before touching any setting again: if curl returns the new content and your browser does not, the purge worked and the browser cache is the remaining problem.

How do I clear the Cloudflare cache?

From the dashboard, open the Caching > Configuration page. Purge Everything clears the whole zone, and Custom Purge lets you purge by URL, hostname, tag or prefix. All five methods are available on every plan, including Free, since April 2025. Purge by single URL is the method Cloudflare recommends, because Purge Everything sends every subsequent request back to your origin at once and can spike its load.

Why is my Cloudflare cache rule not working?

Three causes cover almost every case. The expression does not match the request you are testing, so the rule never fires. Another cache rule lower in the list matches the same request and overrides your setting, because cache rules stack and the last matching rule wins for any conflicting setting. Or the rule fires correctly but Edge TTL is left on one of the options that respect origin headers, so a no-store or private header from your origin still decides the outcome.

I purged the URL but the file is still not purging. What did I miss?

The query string is part of the default cache key, so purging /style.css does not purge /style.css?v=3. Scheme and hostname must also match exactly, and the path is case-sensitive even though the hostname is not. A single-file purge from the dashboard also fails when a cache rule sets a custom cache key based on headers or cookies, or when the rule expression only matches GET requests. Purge by prefix or by tag is the reliable fallback.

How long does a Cloudflare purge take to propagate?

Seconds, not minutes. Cloudflare reports purge latency for tags, hostnames and prefixes consistently under 150 ms, and around 250 ms at the median for single-file purge. If content is still stale a minute later, the purge is not what is slow: you are looking at a browser cache, a mismatched cache key, or a rate-limited purge request that never ran. Free plans are limited to five purge requests per minute per account for hostname, tag, prefix and Purge Everything.

Does cf-cache-status REVALIDATED mean my cache is working?

Partly. REVALIDATED means the object was in the edge cache but Cloudflare asked your origin whether it had changed, and the origin answered that it had not. Bandwidth is saved because the body is served from cache, but the round trip to your origin still happens on every request, so latency and origin request counts barely improve. It is typically caused by must-revalidate or no-cache in your Cache-Control header. Seeing REVALIDATED instead of HIT is the usual sign that a cache is technically enabled but not doing the work you expect.

Purges that change nothing, rules that never fire?

CF Garage reads your headers, reconstructs your cache keys, and puts your cache rules back in a readable order. Fixed pricing, written updates, satisfaction guarantee.

View our offers

Related topics