Cloudflare caches by file extension, and HTML and JSON are not on the default list — so most pages return cf-cache-status: DYNAMIC until a Cache Rule makes them eligible. BYPASS means the origin response blocked caching instead.
The common assumption is that traffic routed through Cloudflare is cached traffic, and that a slow page means the cache is broken. Neither is quite right. Static assets are cached by default, pages are not, and there are several distinct ways a response can end up served from your server — each with a different fix. Guessing between them is how people end up stacking rules that cancel each other out.
Read the header before you change any setting
Every proxied response carries a cf-cache-status header, and it names the decision that was made. Fetch the URL and look at it before touching the dashboard:
curl -sSI https://example.com/ | grep -iE '^(cf-cache-status|age):'The Age header is a useful companion: it reports how many seconds the asset has been sitting in cache, and it appears only on responses actually served from cache. If Age is missing, the response came from your origin, whatever else the page appears to be doing. If the headers are absent entirely, the hostname is probably not proxied at all — see what the orange cloud means in Cloudflare for that case, because a DNS-only record never reaches a cache in the first place.
What each cf-cache-status value means
| Value | What happened | Served from |
|---|---|---|
| HIT | The asset was in cache and still fresh | Cache |
| MISS | Cacheable, but not present in this data center yet | Origin |
| DYNAMIC | Judged not eligible for cache before any lookup happened | Origin |
| BYPASS | Eligible, but the response itself could not be stored | Origin |
| EXPIRED | A stale copy existed; fresh content was fetched | Origin |
| STALE | Expired copy served because the origin could not be reached | Cache |
| REVALIDATED | The origin confirmed the cached copy was unchanged | Cache |
| UPDATING | Expired copy served while a refresh runs in the background | Cache |
| NONE/UNKNOWN | The response was produced before cache was ever consulted | Neither |
That last row catches people out. A Worker that answers without a subrequest, a blocked WAF request, and a redirect rule all short-circuit ahead of the cache, so they are labelled NONE/UNKNOWN rather than describing a cache outcome at all. The full reference lives in the cache responses documentation.
Two decisions, not one
DYNAMIC and BYPASSboth mean “not cached”, and they are routinely treated as the same problem. They are not. DYNAMIC is decided at request time, from the URL and your rules, before the cache is consulted. BYPASS is decided at response time, once your origin has answered and its headers have been read. One is fixed in your Cloudflare configuration; the other is usually fixed on your server.
DYNAMIC: the request never reached the cache
Four causes account for nearly all of them:
- The extension is not on the default list. Eligibility is judged by file extension, not MIME type, and the default list covers images, fonts, archives, CSS, and JS — but deliberately excludes HTML and JSON, since pages and API responses are frequently personalised. A URL with no extension at all, such as
/pricing, falls in the same bucket. - A rule says bypass. A Cache Rule set to bypass, or a legacy configuration or page rule with cache level set to bypass, produces
DYNAMICfor everything it matches. When several rules overlap, Rule Trace shows which one actually applied instead of leaving you to reason about ordering. - The method is not GET or HEAD. Nothing else is cached, so a page whose content arrives via POST cannot be cached no matter how it is configured.
- Development Mode is on. It suspends caching zone-wide for three hours and returns
DYNAMICfor every response. It is the first thing to check when caching stopped working “for no reason” earlier today.
BYPASS: eligible, but the response said no
Here the request cleared eligibility and the answer from your origin is what prevented storage. The usual culprits:
| In the response | Why it blocks caching |
|---|---|
Set-Cookie | Treated as per-visitor by default, so the response is not stored |
Cache-Control: no-store or bare private | Blocks storage regardless of plan or origin cache-control mode |
Cache-Control: no-cache, max-age=0 | On Free, Pro, and Business these produce revalidation rather than a bypass; with origin cache-control disabled, the default on Enterprise, they block it |
Vary: * | Always bypasses, whatever else is configured |
| Body over the size limit | 512 MB on Free, Pro, and Business; 5 GB by default on Enterprise |
Request had Authorization | Cacheable only if the response also carries public, s-maxage, or must-revalidate |
Two of these are worth knowing precisely. First, whether no-cache blocks caching depends on origin cache-control, which is enabled by default on Free, Pro, and Business plans and disabled by default on Enterprise — so identical origin headers behave differently on different plans. Second, since a change in May 2026, responses Cloudflare refuses to cache report BYPASS consistently. Oversized files used to report MISS forever, which was indistinguishable from a broken cache. Older write-ups still describe the previous behaviour, and hit-rate figures from before and after the change are not comparable.
Repeated MISS: the cache key keeps changing
A first MISS in each data center is normal — that request is what populates the cache. A URL that misses every time is a different symptom, and the cause is usually that no two requests share a cache key. The key is built from the origin scheme, host, path, and query string, so:
- Query strings fragment the cache. Every distinct query string is its own entry. Campaign tags, session identifiers, and cache-busting timestamps turn one asset into thousands of entries that are each requested once. Cache Rules can ignore or allow-list specific parameters; sorting them only helps when the same parameters arrive in a different order.
- Custom keys can shard. Adding a cookie or header that varies per user to the key does exactly what you asked and gives every user a private copy.
- Vary multiplies entries. A high-cardinality header listed in the origin’s
Varystores a separate variant per value, and the hit rate collapses. - Low-traffic assets are evicted. If two consecutive requests to the same data center both miss, Tiered Cache is the usual answer for long-tail content.
Before concluding the cache is broken, confirm both test requests reached the same location: the last three characters of the cf-ray header identify the data center. Note also that the scheme in the key is the one used to reach your origin, not the one the visitor used, which is why changing the encryption mode forces the cache to warm up again.
Making pages cacheable on purpose
- Add a Cache Rule matching the paths you want cached, with eligibility set to yes and an explicit edge TTL. The TTL matters: without one, the response falls back to whatever your origin’s headers say.
- Exclude anything personalised — carts, dashboards, admin paths, anything gated by a session cookie — in the same rule expression rather than in a second rule bolted on afterwards.
- Decide what should happen to
Set-Cookie. An edge TTL that ignores origin cache-control will strip it and cache the response, which is correct for anonymous pages and actively dangerous for personalised ones. - Request the URL twice from the same client and confirm you see
HITwith anAgethat climbs. A single request cannot tell you anything.
Cache behaviour is revised more often than most Cloudflare settings — the bypass reporting change above landed in 2026 — so before relying on a specific row here, check the current troubleshooting reference.
FAQ
What does cf-cache-status: DYNAMIC mean?
It means Cloudflare decided the request was not eligible for caching before it ever looked in the cache, so the request went straight to your origin. The usual reason is that the file extension is not one Cloudflare caches by default — HTML and JSON are not — and no Cache Rule says otherwise.
Why is Cloudflare not caching my HTML pages?
Because Cloudflare decides eligibility by file extension, and HTML is deliberately excluded from the default list. Pages are served from your origin until you add a Cache Rule that marks them eligible for cache and gives them an edge TTL.
What is the difference between BYPASS and MISS in Cloudflare?
BYPASS means Cloudflare refused to store the response — it was eligible at request time, but something in the response, such as a Set-Cookie header or an oversized body, made it uncacheable. MISS means the response was cacheable and simply was not in that data center's cache yet, so the next request should be a HIT.
Why does Cloudflare keep returning MISS on every request?
Almost always because each request produces a different cache key. Query strings count toward the key by default, so tracking parameters, session IDs, or timestamps split one asset into thousands of entries that are never requested twice. Low-traffic assets can also be evicted between requests.
Does a Set-Cookie header stop Cloudflare from caching?
By default, yes — a response carrying Set-Cookie is not stored, and it reports BYPASS. You can override that by setting an explicit edge TTL in a Cache Rule that ignores origin cache-control, by having the origin send Cache-Control: private=“Set-Cookie”, or by stripping the header with a response header Transform Rule.