Personal study reference, not customer-facing. Caching is the Cloudflare product every customer already has running and almost none of them fully understand — this covers what's actually cached by default, how to extend it, the real header precedence, and several corrections to commonly-repeated outdated claims (purge methods, Tiered Cache topology naming). Every fact below is sourced from live Cloudflare docs (full list at the bottom).
Caching is on for every customer right now, whether they've touched the settings or not — the gap is almost always that HTML and API responses aren't included by default, and people don't realize it.
Cloudflare caches based on the URL's file extension only, never by the actual Content-Type the origin returns. HTML and JSON are not cached by default — this is the single most common gap customers hit. The default list covers image/video/audio/font/archive/document formats: 7Z, AVI, AVIF, APK, BIN, BMP, BZ2, CLASS, CSS, CSV, DOC, DOCX, DMG, EJS, EOT, EPS, EXE, FLAC, GIF, GZ, ICO, ISO, JAR, JPG, JPEG, JS, MID, MIDI, MKV, MP3, MP4, OGG, OTF, PDF, PICT, PLS, PNG, PPT, PPTX, PS, RAR, SVG, SVGZ, SWF, TAR, TIF, TIFF, TTF, WEBM, WEBP, WOFF, WOFF2, XLS, XLSX, ZIP, ZST. robots.txt is cached by default too, as a special case. Only GET and HEAD requests are ever cached — no exceptions.
| Status code | Default TTL |
|---|---|
| 200, 206, 301 | 120 minutes |
| 302, 303 | 20 minutes |
| 404, 410 | 3 minutes |
| All other codes | Not cached by default |
cf-cache-status reference| Value | Meaning |
|---|---|
| HIT | Served from Cloudflare's cache |
| MISS | Not in cache; Cloudflare fetched from origin and (usually) stored it |
| DYNAMIC | Cloudflare decided before even checking the cache that this isn't eligible — see below |
| BYPASS | Was eligible, but the origin response or a rule ultimately prevented storage — see below |
| EXPIRED | Was cached, TTL ran out; will refresh from origin on this request |
| REVALIDATED | Was cached, TTL expired, origin confirmed via 304 it's still fresh |
| UPDATING | Stale-while-revalidate: served from cache while an async refresh happens in the background |
| STALE | Origin is down — stale cache served anyway rather than an error |
| NONE/Unknown | Not cached — usually a non-GET/HEAD method or a zero TTL |
Not present at all on: 3xx redirects produced by Cloudflare itself (Redirect Rules, Bulk Redirects), WAF blocks, Cloudflare error pages, or Worker-originated fetch requests that don't go through the cache.
DYNAMIC vs. BYPASS — the distinction people blur together. DYNAMIC means Cloudflare decided "not eligible" before ever checking the cache — wrong file extension, a Bypass-cache rule matched, non-GET/HEAD method, or Development Mode is on (which suspends caching for 3 hours zone-wide). BYPASS means the request was eligible, but the actual response couldn't be stored — response exceeded the plan's max cacheable size, or the origin sent no-store/bare private. Critically: no-cache, max-age=0, and s-maxage=0 do NOT produce BYPASS — those produce MISS on the first request, then REVALIDATED or EXPIRED after. If a customer says "I set max-age=0 to stop caching," gently correct them: that still caches it, just marks it stale immediately. The directive that actually means "don't cache" is no-store.
When a response relies on heuristic freshness (no explicit Cache-Control), Cloudflare automatically runs Cache Deception Armor to defend against cache deception attacks. It only inspects responses whose Content-Type starts with text/ or application/ — the high-risk types for accidentally caching generated, user-specific content under a static-looking URL. If the request's file extension implies a MIME type (e.g. .css → text/css) that doesn't match the actual Content-Type returned, the response is refused caching and marked BYPASS. Setting any explicit Cache-Control directive skips this check entirely — it only activates when freshness is being guessed.
The current, supported way to control caching — Page Rules are legacy, don't lead with them.
(http.host eq "example.com") or a path matchControls what Cloudflare treats as "the same request" for caching purposes. Can include or exclude: query strings (high-cardinality query params fragment the cache badly if not normalized), specific headers, cookies, device type, country (CF-IPCountry), or language (Accept-Language). Be deliberate about cardinality — adding too many variations to the cache key means near-zero hit rate since almost every request becomes "unique."
Common pattern for logged-in users on an otherwise-cacheable page: add a condition that bypasses when a session/auth cookie is present, e.g. AND cookie does not contain session_id. This lets the same URL serve cached content to anonymous visitors while always hitting origin for authenticated ones.
The real, documented precedence — this gets mis-stated often, including in older internal material.
The actual precedence order is three tiers, not five: Cloudflare-CDN-Cache-Control > CDN-Cache-Control > Cache-Control. Within each of those headers, standard HTTP semantics apply (s-maxage for shared/CDN caches takes priority over plain max-age; the legacy Expires header is the lowest-priority fallback, used only when nothing else is present). An origin that returns Cache-Control: public, max-age=3600 together with CDN-Cache-Control: no-store produces BYPASS — the CDN-specific header wins even though the general one says cache it.
| Plan | Origin Cache Control default |
|---|---|
| Free, Pro, Business | Enabled by default |
| Enterprise | Disabled by default — must be turned on manually |
When disabled, Cache-Control: no-cache, max-age=0, or s-maxage=0 from the origin don't produce the normal MISS→REVALIDATED/EXPIRED behavior — instead they're treated as no-store and the response isn't cached at all. If an Enterprise customer's cache-control headers seem to be "ignored," check this setting first.
A Cache Rule with "ignore cache-control header and use this TTL" overrides all of the above — origin headers become irrelevant for that matched traffic.
The free, single-highest-leverage toggle most customers haven't turned on. Solves a completely different problem than Cache Rules — Cache Rules reduce origin load on hits; Tiered Cache reduces origin load on misses.
Cloudflare has 330+ data centers. Without Tiered Cache, every one of them can independently ask the origin for a cache miss — a brand-new or just-expired piece of content can trigger an origin stampede: dozens of data centers worldwide hitting the same origin for the same file simultaneously.
Data centers split into lower tiers (closest to visitors) and upper tiers (a small set closer to origin). A lower-tier miss asks an upper tier first, not the origin directly. Only upper tiers are permitted to contact origin at all. The practical effect: instead of 330+ data centers hammering origin, misses funnel through a handful of upper tiers — the first one fills the cache, everyone else pulls from it. Bonus: origin sees connections from a small, stable set of data centers instead of the whole network, reducing connection churn.
| Topology | How it picks the upper tier | Plan |
|---|---|---|
| Smart (default) | Dynamically selects the single closest upper tier per origin using real latency data — zero configuration | All plans, free |
| Generic Global | Consistent-hash of the cache key into regional upper-tier pools; prioritizes regional locality and shared-cache efficiency over per-origin latency | All plans, free |
| Regional | Adds a regional hub check before reaching an upper tier outside the region — for Smart/Custom topologies with upper tiers concentrated in one or two regions | Enterprise (bundled in Smart Shield Advanced) |
| Custom | Bespoke topology, configured with your account team | Enterprise |
The public-cloud origin gotcha: origins on AWS, GCP, Azure, or Oracle Cloud use anycast/regional unicast networking that defeats Smart Tiered Cache's latency-probing — it can't tell where the origin actually is. Fix: set a cloud region hint (Caching → Tiered Cache → Origin Configuration, e.g. aws:us-east-1), which is free on every plan including Free. Without the hint, an anycast-fronted origin falls back to a less effective multi-upper-tier topology with lower hit rates. Also worth knowing: changing origin IPs/DNS while Smart Tiered Cache is active can reassign upper tiers and cause a temporary MISS spike while cache refills.
Check unicast vs. anycast before promising a region hint: region hints only help origins behind anycast. If the origin is on unicast (one fixed location), Smart Tiered Cache can already locate it through latency probing, and on Caching → Tiered Cache → Origin Configuration that origin shows a lock next to it with its assigned upper tiers (a primary plus a fallback). A locked origin can't take a region hint, and a hint wouldn't change anything. Before the call, open Origin Configuration: if you see the lock, skip the hint recommendation and focus on cache rules and cache keys instead. Only origins without the lock (anycast) are candidates for a hint.
Confirm Tiered Cache actually fired on a request via the CacheTieredFill field in Logpush http_requests logs. Separately: Workers have their own always-on, two-tier cache (lower tier near the eyeball, upper tier aggregating across the network) — this runs automatically for every Worker regardless of zone-level Tiered Cache settings, and isn't something you configure.
Cloudflare now sells Tiered Cache, Connection Reuse, Argo, Regional Tiered Cache, and Cache Reserve as one bundled product — Smart Shield — rather than as separate standalone toggles. Argo in particular is commonly confused with caching; it isn't caching at all, and it's the one piece of this bundle that's genuinely a buying decision rather than a free win.
| Package | Includes | Availability |
|---|---|---|
| Smart Shield (base) | Smart Tiered Cache + Connection Reuse | Free, Pro, Business, Enterprise — opt-in, no cost beyond plan |
| Smart Shield + Argo | Base package + Argo Smart Routing | Free, Pro, Business, Enterprise — purchasable self-serve |
| Smart Shield Advanced | Base + Argo + Regional Tiered Cache + Cache Reserve (+ Dedicated CDN Egress IPs) | Enterprise only — not available for self-serve purchase on Free/Pro/Business, contact Enterprise Sales |
Configured from Speed → Smart Shield in the dashboard (not the old standalone Caching menu) — select a package and follow the guided onboarding flow. Health Checks are available across all packages on Pro and above.
Separate mechanism from Tiered Cache, bundled into every Smart Shield package including the base one. When upper-tier data centers need to reach your origin, Smart Shield batches multiple requests over a single shared TCP connection instead of opening a new one per request — this reduces total connections to origin by 30% on average. With HTTP/2, this extends into connection coalescing: requests for different hostnames can share one connection, as long as both resolve to the same IP/port and the origin's TLS certificate covers both hostnames in its SAN list.
When multiple requests hit the same Cloudflare data center simultaneously for the same uncached asset, Cloudflare uses a cache lock: only the first request is forwarded to origin, the rest wait and then have the response streamed to them once it arrives. This is called request collapsing in Cloudflare's own docs — you'll sometimes hear it called "request coalescing" informally, but that's not the documented product term. It's part of default cache behavior (not Smart-Shield-specific) and is explicitly scoped to a single data center in the documentation — don't claim it also compounds at the upper-tier layer unless/until that's separately confirmed.
Not caching. Argo optimizes the network path for traffic that has to reach your origin anyway — cache misses, dynamic pages, API calls, checkout flows, anything personalized. Without Argo, that traffic takes whatever path the public internet's BGP routing happens to choose that second, which can mean congestion or a suboptimal route. Argo continuously measures real-time latency and packet loss across paths and routes traffic over the fastest one available. Benefits are most apparent the farther your users are from your origin; built-in Analytics let you compare TTFB with Argo on vs. off.
Pricing correction: Argo used to be sold standalone with its own per-GB metered billing. That's no longer how it's offered — Argo is now exclusively accessed via the Smart Shield + Argo or Smart Shield Advanced packages. Don't quote old standalone per-GB pricing to a customer; point them to the Smart Shield package selector in the dashboard instead.
Do the free work first: confirm caching hygiene, then Smart Shield base. Only pitch Argo once there's real uncacheable, distance-sensitive traffic to justify it — customers who skip straight to a paid network-optimization add-on are sometimes just papering over content that should have been cacheable to begin with.
The ultimate upper-tier — persistent storage (built on R2) that holds content far longer than the standard cache would, specifically to shield origin from egress costs.
Content-Length response header presentStarts at a 30-day retention period by default; an asset not requested within that window is evicted, and any request resets the clock. As of a September 2026 change, purging by tag, hostname, prefix, or "everything" now forces a Cache Reserve miss, not just a revalidation — the next request must pull the full content from origin again, and writing it back to Cache Reserve is billed as a Class A operation. Purge-by-URL already removed content outright and is unchanged. If a customer purges broadly and frequently, this is a real cost consideration worth flagging — a separate invalidate_cache endpoint exists specifically to revalidate instead of fully evict, which is the cheaper option when the goal is just "make sure this gets re-checked," not "force it to disappear immediately."
Usage-based pricing (storage + operations); requires a paid Cache Reserve plan, but you can keep using Cloudflare's regular CDN for free without it.
Correction to a commonly repeated claim: as of April 2025, all five purge methods are available on every plan, including Free — Purge Everything, Purge by Prefix, Purge by Hostname, Purge by URL (single-file), and Purge by Tag. This used to be Enterprise-only and some older internal material still says that; it's no longer accurate.
| Method | Use for |
|---|---|
| Purge by URL | One specific asset — the most precise option |
| Purge by Tag | Grouped invalidation via Cache-Tag response headers — cleanest for "everything related to this product/post changed" |
| Purge by Prefix | Everything under a path, e.g. /static/ |
| Purge by Hostname | Useful for staging/preview deployments — up to 100 hostnames at once |
| Purge Everything | The nuclear option |
Purging by hostname (and similarly prefix/tag/everything) results in MISS on the next request normally — but with Tiered Cache enabled, it may instead show EXPIRED, since the lower tier tries to revalidate with the upper tier first rather than going straight to origin. All methods are available via API for CI/CD integration, not just the dashboard.
Reach for this when Cache Rules genuinely aren't expressive enough — not as a default approach.
From inside a Worker, the cf object on fetch() can override Cache Rules settings per-request: cacheEverything (treat as static regardless of extension), cacheTtl, cacheTtlByStatus (different TTLs per status-code range, e.g. {"200-299": 86400, 404: 1, "500-599": 0}), cacheKey (Enterprise only), and cacheTags for later targeted purging.
Workers also get a dedicated Cache API: ctx.cache.purge() (by tags, pathPrefixes, or purgeEverything — fields are unioned, not intersected, when combined) and an equivalent invalidate path. Note: pathPrefixes matches path starts only — no scheme/host/query — and is scoped to the calling entrypoint only; a purge from one Worker entrypoint won't affect cache stored by a different entrypoint even on overlapping paths. For exact-match-only semantics with zero over-purge risk, use a tag instead of a prefix.
WordPress-specific, roughly $5/month add-on. Automatically caches HTML (the exact gap most customers have by default) and auto-purges on publish — no manual Cache Rule authoring required for the common WordPress case.
| Scenario | Use |
|---|---|
| Static assets (images, CSS, JS, fonts, video) | Default cache — already on, no config |
| HTML pages, WordPress | APO |
| HTML pages, static site generator (Jekyll/Hugo/Astro) | Cache Rule, long Edge TTL, purge on deploy |
| HTML pages, dynamic with logged-in users | Cache Rule that bypasses on the auth/session cookie |
| Public API responses | Cache Rules; Workers + Cache API if logic is too complex for a rule |
| Per-user API responses | Workers KV — cache the underlying data, not the HTTP response itself |
| Large files, frequently accessed | Default cache + Tiered Cache |
| Large files, rarely accessed but must stay available | Cache Reserve |
| Object storage | R2 — zero egress fees |
| App state inside a Worker | KV (eventually consistent) or Durable Objects (strongly consistent) |
| Dynamic/uncacheable traffic (APIs, checkout, personalized), origin far from users | Argo Smart Routing (Smart Shield + Argo) |
| Origin getting too many simultaneous connections, not just too many requests | Connection Reuse (included in every Smart Shield package, incl. base) |
| Feature | Plan |
|---|---|
| Default cache, basic Cache Rules, Smart/Generic Global Tiered Cache, all 5 purge methods | All plans, including Free |
| Smart Shield (base): Smart Tiered Cache + Connection Reuse | All plans, including Free — opt-in |
| Smart Shield + Argo (adds Argo Smart Routing) | Self-serve purchase on Free, Pro, Business, Enterprise |
| APO for WordPress | ~$5/mo add-on |
| Custom Cache Keys, Cache by Cookie/Header | Pro and above |
| Cache Reserve | Usage-based (storage + operations) |
Smart Shield Advanced (Regional Tiered Cache + Cache Reserve bundled), Regional/Custom Tiered Cache standalone, non-standard caching ports, Origin Error Pass-through, Worker cacheKey override, Dedicated CDN Egress IPs | Enterprise |
| R2 | Usage-based, zero egress fees |
Real curl commands showing DYNAMIC → MISS → HIT live, pulled from the companion demo script.
curl -sI https://nginx.tarheel.us/ | grep -iE "cf-cache|cf-ray|age"
// Expect: cf-cache-status: DYNAMIC — no file extension on "/", so it was never considered for caching
for i in 1 2 3; do curl -sI https://nginx.tarheel.us/ | grep cf-cache; done
// Three requests, three DYNAMICs, three round trips to origin
// Expression
(http.host eq "nginx.tarheel.us")
// Cache eligibility: Eligible for cache
// Edge TTL: Override origin -> 2 hours (any duration works for a demo)
for i in 1 2 3 4; do curl -sI https://nginx.tarheel.us/ | grep cf-cache; sleep 1; done
// cf-cache-status: MISS <- first request, fetched from origin and stored
// cf-cache-status: HIT <- served from the edge
// cf-cache-status: HIT
// cf-cache-status: HIT
One MISS populates the cache, everything after is a HIT — origin is no longer involved. At real traffic volume the ratio is far more dramatic than this small demo (often one MISS per thousand-plus HITs). For an extra visual, grep the age header too and watch it climb on repeated requests — that's edge time, not origin time.
Harder to demo with a single curl command since it requires multiple data centers to actually be hit, but the dashboard proof works well: after enabling Tiered Cache, check the CacheTieredFill field in Logpush http_requests logs to confirm it's actually engaging, and if the origin is on a public cloud, set the cloud region hint (Caching → Tiered Cache → Origin Configuration) in the same session so Smart Tiered Cache can actually locate it.
Argo is genuinely hard to demo with curl since the benefit is a network-path optimization you can't see client-side in a single request. Use the interactive visual demo instead — cf-demo-app.dustinburke23nc.workers.dev/argo shows a world map of traffic flowing from any region to a US-based origin, Argo on vs. off, with hop count, latency, and packet loss compared live. Pair it with the Analytics TTFB comparison (with Argo vs. without) for a customer's own domain once they've actually enabled a Smart Shield + Argo package.
The framing that lands with a CFO in the room: "every byte here is a byte your origin didn't have to send — if you're paying for egress on AWS or GCP, this is real money saved." Point at Cached Bandwidth in Analytics & Logs → Traffic (or Caching → Overview) specifically when that audience is present.
"My HTML isn't being cached — is something broken?"
No — HTML is never cached by default, regardless of plan. It's not a bug; URLs without a static file extension are assumed potentially personalized. Fix with a Cache Rule setting Cache Eligibility to "Eligible for cache," or APO if it's WordPress specifically.
"I set max-age=0 — why is it still showing as cached?"
max-age=0 means cached but immediately stale, not "don't cache." It'll show MISS then REVALIDATED/EXPIRED on subsequent requests, not BYPASS. If the goal is genuinely no caching at all, the origin needs to send no-store.
"Our origin is getting hammered — what's the fastest fix?"
Turn on Tiered Cache first — it's free, a single toggle, and specifically targets the origin-stampede problem that Cache Rules alone don't solve. Cache Reserve is the next step for content that's expensive to regenerate or accessed rarely but must stay available.
"Can we purge by cache tag on our plan?"
Yes, on any plan — this was Enterprise-only in the past but has been available to everyone since April 2025. Worth double-checking this against any older internal reference material that might still say otherwise.
"Should we cache by IP address for personalization?"
No — IPs are shared via CGNAT, corporate proxies, and NAT, so this leaks one user's cached content to another. Use a cookie, header, or Workers + KV instead.
"Page Rules or Cache Rules?"
Cache Rules — Page Rules are legacy. Don't lead a new configuration with Page Rules even though they still technically function.
Click a question to reveal the answer. No peeking before you commit to one.
No — HTML and URLs without a static file extension are never cached by default on any plan. This is expected behavior, not a bug. Fix with a Cache Rule (Eligible for cache) or APO for WordPress.
False. It's still cached, just immediately marked stale — you'll see MISS then REVALIDATED/EXPIRED on later requests, not BYPASS. The directive that actually prevents caching is no-store.
DYNAMIC means Cloudflare decided "not eligible" before ever checking the cache (wrong extension, non-GET/HEAD, Bypass rule, Dev Mode). BYPASS means the request was eligible, but the actual response couldn't be stored (too large, no-store/private from origin).
Cloudflare-CDN-Cache-Control > CDN-Cache-Control > Cache-Control — three tiers, not five. An origin can send a permissive Cache-Control but still get BYPASSed if CDN-Cache-Control: no-store is also present.
Whether Origin Cache Control is enabled — it's disabled by default on Enterprise (unlike Free/Pro/Business, where it's on by default). When disabled, directives like no-cache/max-age=0 get treated as no-store instead of the normal revalidation flow.
Origin load from cache misses. Without it, any of Cloudflare's 330+ data centers can independently hit origin on a miss, causing a stampede. Cache Rules only help with hits (serving from cache once something's there); Tiered Cache funnels misses through a small set of upper tiers instead.
AWS (and GCP/Azure/Oracle Cloud) use anycast/regional networking that defeats Smart Tiered Cache's latency-probing. Fix: set a cloud region hint (e.g. aws:us-east-1) — free on every plan including Free.
False. Smart and Generic Global are free on all plans. Regional and Custom Tiered Cache are Enterprise-only (Regional is bundled into Smart Shield Advanced).
Cacheable under standard rules, a freshness TTL of at least 10 hours, a Content-Length header present, and — for Image Transformations — only original files qualify, not resized variants.
Since a 2026 change, purging by tag/hostname/prefix/everything forces a full Cache Reserve miss (not just revalidation) — the next request pulls fully from origin and re-writes to Cache Reserve as a billed Class A operation. If the goal is just "re-check freshness," the invalidate_cache endpoint is the cheaper option instead.
False — as of April 2025, all five purge methods (Everything, Prefix, Hostname, URL, Tag) are available on every plan including Free. This used to be Enterprise-only; some older material still incorrectly says it is.
cacheKey changes what counts as "the same request" for caching purposes and is Enterprise only. cacheTags appends tags for later targeted purging and has no such plan restriction.
No. IPs are shared across many real users via CGNAT, corporate proxies, and NAT — caching by IP leaks one user's personalized content to another. Use a cookie, header, or Workers + KV instead.
No — only GET and HEAD methods are ever cached, with no exceptions in the standard cache. (Workers' Cache API can be used to intentionally cache non-standard scenarios, but that's a deliberate opt-in, not default behavior.)
It protects against cache deception attacks by checking that a URL's implied MIME type (from its file extension) matches the actual returned Content-Type, but only for text/* and application/* responses relying on heuristic freshness. Setting any explicit Cache-Control directive skips the check entirely.
Trick framing — you can't buy Argo standalone anymore. It only comes bundled: the Smart Shield + Argo package includes Smart Tiered Cache + Connection Reuse (the base package) plus Argo on top. There's no way to get Argo without also getting the base Smart Shield features.
False. The documented term is request collapsing, implemented via a cache lock — only the first request reaches origin, the rest wait and get the response streamed to them. "Coalescing" is sometimes used informally but isn't the term in Cloudflare's docs (and is easy to confuse with HTTP/2 connection coalescing, which is a different mechanism entirely).