# Prove the Edge Is Answering in 2026: One Response Can Report a Cache Hit and DYNAMIC at the Same Time

> A CDN in front of WordPress does not mean the edge serves your HTML. Two headers, two requests, and the one word that says whose fault an uncached page is.

- Published: 2026-10-04
- Author: xSpeed Cache Team
- Tags: CDN, Cloudflare, Tutorial, Caching, platform:cloudflare, check:D5
- Canonical: https://xspeedcache.com/blog/check-cdn-caching-wordpress/

---

Updated October 2026

Cloudflare's default cache behavior documentation, fetched 4 October 2026, lists **56 file extensions** it caches without being asked, and states HTML is not one of them. Our own probe of 556 Cloudflare-fronted WordPress sites found **330 returning `cf-cache-status: DYNAMIC`**, meaning no cache lookup happened. The check most people run is a single `curl -I`, and what they read is a single header. One header cannot settle this. A response can report a cache hit and `DYNAMIC` in the same breath, because the two words describe two different caches, and two managed WordPress hosts do exactly that. Here is how to read the receipt.

## Quick Summary: What the Status Word Tells You to Do Next

| If you see this on your HTML | Do this | Why |
|:---|:---|:---|
| `HIT` with an `Age` | Nothing | It came out of edge storage |
| `DYNAMIC` | Add a cache rule marking the document eligible | Nothing looked in the cache |
| `BYPASS` | Fix the origin response, not the rule | It was eligible and the origin refused |
| A host `HIT` beside `DYNAMIC` | Treat both as true | Your origin cached it, the edge did not |

![Nine Cloudflare cache status values, whether each means an edge answered, and whether Cloudflare sets Age](https://xspeedcache.com/images/blog/cdn-cache-status-by-layer.webp)

## The Document Is the Only Response That Counts

Cloudflare's [default cache behavior documentation](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/) states that it "only caches based on file extension and not by MIME type", and that "the Cloudflare CDN does not cache HTML or JSON by default". Counted from that page on 4 October 2026, the list holds 56 extensions, `JPG`, `CSS` and `MP4` among them. A WordPress pretty permalink ends in a slash and carries no extension, so it cannot land on that list by accident.

A report can therefore show a CDN in front while every page still comes from your server. The images are at the edge. The document is not. That gap is what [our CDN adoption study](https://xspeedcache.com/blog/wordpress-cdn-adoption-ttfb-study/) measured, and it is the half of the wait behind [a page that is fast for you and slow abroad](https://xspeedcache.com/blog/slow-for-visitors-abroad/).

## Two Hosts, One Response, Two Verdicts

A GET with a browser user agent on 4 October 2026 returned this from `kinsta.com/blog/`:

```
cf-cache-status: DYNAMIC
x-kinsta-cache: HIT
```

And this from `wpengine.com/blog/`:

```
cf-cache-status: DYNAMIC
x-cache: HIT: 5
x-cacheable: SHORT
```

Both are telling the truth. The page is cached at the host's own layer, and Cloudflare proxied it without a cache lookup. Read only the vendor status and nothing is cached; read only the host header and the edge answers. Neither survives the other, which is why the test is a pair.

The vocabulary differs per vendor, and every value below was read live that day:

| Front end | Header to read | Value measured 4 October 2026 |
|:---|:---|:---|
| Cloudflare | `cf-cache-status`, `cf-ray` | `HIT`, ray suffix `IAD` |
| Fastly | `x-cache`, `x-cache-hits`, `x-served-by` | `MISS, HIT` and `0, 4` |
| CloudFront | `X-Cache`, `X-Amz-Cf-Pop` | `Hit from cloudfront`, `IAD61-P11` |
| BunnyCDN | `cdn-cache`, `cdn-requestcountrycode` | `HIT`, `US` |

Those came from `wpbeginner.com`, `python.org`, `docs.aws.amazon.com` and `bunny.net`; KeyCDN and Sucuri answer `x-cache` and `x-sucuri-cache` the same way. Fastly prints one value per node in the chain, so `MISS, HIT` is two caches in one string, not a contradiction. None of it is standardised.

## `DYNAMIC` and `BYPASS` Name Two Different Culprits

These two carry the diagnosis and are read as synonyms. Cloudflare's documentation is explicit: `DYNAMIC` "is only returned when Cloudflare determines the asset is not eligible for cache at request time", while a rule marking HTML eligible plus an origin returning a non-cacheable directive produces "`CF-Cache-Status: BYPASS` — not `DYNAMIC`".

| Status | Decided at | Cause on a WordPress site | What to change |
|:---|:---|:---|:---|
| `DYNAMIC` | Request time | No rule marks the document cacheable, a bypass rule matched, or Development Mode is on | The rule, in your dashboard |
| `BYPASS` | Response time | A `Set-Cookie`, `Cache-Control: no-store` or `private`, a `Vary: *`, or an `Authorization` header | The response your server sends |

The `Set-Cookie` row bites WordPress hardest: a comment form, a session notice or a cart on a [WooCommerce store](https://xspeedcache.com/use-cases/woocommerce/) can attach one to an anonymous page view. The rule is then perfect and the page still uncached.

![DYNAMIC decided at request time against BYPASS decided at response time, with the fix each one needs](https://xspeedcache.com/images/blog/dynamic-vs-bypass-decision.webp)

## The `Age` Header Settles Nothing, in Either Direction

`Age` looks like the clean answer and is the most misread header here. Both halves of the problem are written down. [RFC 9111 section 5.1](https://www.rfc-editor.org/rfc/rfc9111.txt) says the presence of an `Age` field "implies that the response was not generated or validated by the origin server for this request", then adds the sentence nobody quotes: "lack of an `Age` header field does not imply the origin was contacted". A missing `Age` is evidence of nothing.

Cloudflare's [cache responses documentation](https://developers.cloudflare.com/cache/concepts/cache-responses/), updated 4 September 2026, closes the other half. It sets `Age` on `HIT`, `STALE` and `UPDATING`, and not on `MISS`, `DYNAMIC`, `BYPASS` or `NONE/UNKNOWN`. But "if the origin sets `Age` on an uncacheable response, that value is proxied to the client unchanged". An `Age` beside `DYNAMIC` came from your server. Under Tiered Cache it can also be inherited from an upper tier, so it can be older than the local copy.

Read the status word first, and use `Age` only on a copy already confirmed.

## Send a GET, Not a HEAD, and Send It Twice

`curl -I` sends a HEAD request. Cloudflare's default behavior page lists "the HTTP request method is anything other than a `GET`" among the conditions under which it does not cache a resource, and an origin cache can refuse a HEAD too.

Send a GET and throw the body away instead:

```
curl -s -D - -o /dev/null -A 'Mozilla/5.0' https://example.com/your-page/
```

Then send it again. A first request can store the page on its way past, which is what `MISS` means, so only a second shows storage being read. One request can disprove a cache. It can never prove one.

The origin layer refuses a HEAD for its own reasons, and ours says so out loud. Our [cache HITs and MISSes guide](https://xspeedcache.com/docs/hits-and-misses/) documents `X-XSpeed-Reason: non-get` as a gate producing a `BYPASS`, and names the HEAD request `curl -I` sends as a case of it. xSpeed Cache is ours, built by WPDeveloper, and that header exists to make the second verdict a read instead of a guess.

## The Edge Receipt: Four Questions, One Header Each

Run these on one real post URL, never the home page, which is the URL a misconfigured rule most often gets right.

1. **Is a CDN in front at all?** Read `server` and `via`. No vendor name in either means you are reading your origin.
2. **Did the edge look in its cache for this document?** Read the vendor status. `DYNAMIC` or `NONE/UNKNOWN` means no lookup.
3. **Did this response come out of storage?** Read the status on a second GET. `HIT`, `STALE` and `UPDATING` qualify. `MISS` twice does not.
4. **Which edge answered?** Read the `cf-ray` suffix, `X-Amz-Cf-Pop` or the `x-served-by` colo code, and compare it with where your audience is.

**What it cannot tell you.** It reads one URL, from one place, at one moment, logged out. Each data centre caches separately, so it says nothing about another continent, and a signed-in request is a different cache key.

## Start With the Scan, and Check by Hand Second

Check **D5, "CDN edge serves the page"**, answers this for any URL in one pass. It carries 3 points of the delivery dimension, and a scan run on 4 October 2026 returned a pass with the evidence `Cloudflare answered this page from its edge (cf-cache-status: HIT)`. It is free and needs no account.

Run the free scan on your own site: [xspeedcache.com/scan/](https://xspeedcache.com/scan/)

By hand, the two-request GET above is the method. [Connecting Cloudflare to WordPress](https://xspeedcache.com/blog/cloudflare-wordpress-cache-setup/) covers the setup it checks, our [Cloudflare guide](https://xspeedcache.com/docs/cloudflare/) says which parts of that panel are free and which are Pro, and [Free vs Pro](https://xspeedcache.com/free-vs-pro/) is the full split.

## Reading the Receipt Across Twenty-Five Client Sites

Four headers per page is fine for one site and unworkable for fifty, where the question becomes which site stopped being cached. That is monitoring rather than a `curl` job, and [xSpeed Hub](https://xspeedcache.com/xspeed-hub/) is our own fleet console for xSpeed Cache sites: cache state and hit ratio per site in one view. The origin half is worth standardising too, since a page cache that names its layer makes the second verdict a read. [Checking whether WordPress caching works](https://xspeedcache.com/blog/check-wordpress-caching-working/) covers it.

## Common Mistakes People Make Proving a CDN Works

- **Reading an asset and reporting on the document.** A `HIT` on `style.css` is the default and proves nothing about a page.
- **Testing with one request.** A `MISS` stores the page. The second request is the measurement.
- **Trusting `Age` alone.** Absence proves nothing and presence can be your origin's own value.

## Frequently Asked Questions

### I turned on Cloudflare and `cf-cache-status` still says `DYNAMIC` on every page. What did I miss?

Nothing. This is the documented default: HTML is not on the extension list, so the document is not eligible until a cache rule says so. [Cloudflare APO against an origin page cache](https://xspeedcache.com/blog/cloudflare-apo-vs-caching-plugin/) compares the two routes.

### I added a cache rule for HTML and now I see `BYPASS` instead. Is that progress?

Yes, and a different problem. `DYNAMIC` meant nothing looked. `BYPASS` means the lookup happened and your origin refused it, usually with a `Set-Cookie` or a `no-store`.

### My `curl -I` says `BYPASS` but the page looks cached in a browser. Which one is right?

The browser, probably. `curl -I` sends a HEAD, and both Cloudflare's rules and an origin cache can treat a non-`GET` as uncacheable. Re-run as a GET with the body discarded.

### I get `MISS` every single time I check. What am I doing wrong?

Check whether the URL carries a query parameter, and whether you are signed in. Both change the cache key, so every request is a first one. Our [cache exclusions guide](https://xspeedcache.com/blog/wordpress-cache-exclusions-setup/) covers the fields involved.

### Do I still need a page cache on the origin if the edge is serving?

Yes, for every request the edge cannot serve: the first visitor in each region, signed-in sessions, anything excluded. xSpeed Cache, WP Rocket and LiteSpeed Cache all do it, and ours is free.

### My scan says a CDN was detected but the edge is not serving. Is it contradicting itself?

No. Detection and delivery are separate findings, and separate lines in the report.

### The `Age` on my page is 400,000 seconds. Should I purge?

Only if the content changed. A large `Age` on a `HIT` means the edge held a good copy for a long time, which is the point.

### I cannot prove the edge is answering for a logged-in user.

Read the headers on a signed-in request and you will usually find a `BYPASS`. That is correct, and [cached pages showing logged-in content](https://xspeedcache.com/blog/cached-pages-showing-logged-in-content/) is the failure you would rather not have.

### My two probes disagree, one `HIT` and one `MISS`, within a minute.

Two data centres answered, each with its own copy. Compare the `cf-ray` suffix or `x-served-by` colo code and read them as two edges, not one flapping one.

### I fixed the edge and my Largest Contentful Paint did not move.

Expected. This moves the first byte, a share of Largest Contentful Paint and not the whole of it. The split is in our CDN adoption study above.

## Conclusion: Two Headers, Two Requests, One Real URL

| If you want to | Read this | On |
|:---|:---|:---|
| Know whether an edge answered | The vendor status word | A second GET to a real URL |
| Know whose fault an uncached page is | `DYNAMIC` against `BYPASS` | The same response |
| Know which edge answered | `cf-ray`, `X-Amz-Cf-Pop` or `x-served-by` | Both probes, compared |

**What to do this week:** run the two-request GET on one real post URL, not the home page. Write down the status word and whether an `Age` came with it. On `DYNAMIC`, add a cache rule for your document. On `BYPASS`, find the `Set-Cookie`. Then [scan the same URL](https://xspeedcache.com/scan/) and check that D5 agrees. If the origin half is what you cannot read, xSpeed Cache is free on [wordpress.org](https://wordpress.org/plugins/xspeed/) and prints a layer-named receipt header; Pro is $29 a year at the founding price with a 14-day money-back guarantee, and the [pricing page](https://xspeedcache.com/pricing/) has the tiers.

If your two headers disagree in a way this does not cover, write it up. The vocabularies keep diverging.
