Cloudflare Tiered Cache and Hashed Asset Propagation

A fingerprinted bundle does not “propagate” to Cloudflare’s network the way a DNS record propagates. Nothing is pushed. Each of roughly 330 data centres learns about app.a1b2c3d4.js only when a real user in that city asks for it, and Tiered Cache decides who answers that first request — your origin, or a Cloudflare data centre that already has the file.

What Actually Happens to a New Hash After Deploy

At the moment your deploy finishes, exactly zero Cloudflare data centres hold the new bundle. The first user in Warsaw generates a miss at the Warsaw PoP. Without Tiered Cache, that miss goes straight to your origin. So does the first miss in Denver, in Lagos, in Osaka. The origin serves the same immutable bytes once per data centre that has traffic, which for a global audience means dozens to hundreds of identical fetches concentrated in the minutes after a release.

Tiered Cache inserts a second cache layer between the two. Lower-tier PoPs — the ones users connect to — send their misses to an upper-tier PoP instead of your origin. The upper tier fetches once; every subsequent lower-tier miss is answered from inside Cloudflare’s network.

Miss fan-in through the upper tier Five lower-tier points of presence each miss on a newly deployed hashed asset. All five misses converge on one upper-tier data centre, which makes a single request to the origin. lower-tier PoPs upper tier your origin Warsaw Denver Lagos Osaka Santiago Upper-tier PoP acts as origin shield fetches the object once Origin 1 request, not 5 Without the upper tier each PoP would fetch the same bytes from origin
Every lower-tier miss on a new hash funnels into one upper-tier fetch. The origin serves the object once regardless of how many cities woke up at the same time.

The header that tells you which layer answered is CF-Cache-Status, and it always reports the lower tier’s view. A MISS at the edge that was satisfied by the upper tier still reads MISS — what gives it away is a small non-zero Age on the response, because the upper tier’s copy was already a few seconds or minutes old. That single signal is how you verify the topology is doing anything at all, and it is covered in detail alongside the other cache-debugging headers in the guide to fingerprinting in HTTP headers.

The Three Topologies

Cloudflare offers three arrangements of the upper tier, and they differ in who chooses the upper-tier data centre and how many of them exist.

Three Tiered Cache topologies Three stacked panels. Smart Tiered Cache picks one upper tier automatically, Generic Global uses a single upper tier you nominate, and Regional Tiered Cache adds a regional hop before the upper tier. Three ways Cloudflare arranges the upper tier Smart Tiered Cache Cloudflare picks the upper tier from live latency data edge upper origin Generic Global Tiered Cache You nominate one upper-tier data centre for the whole zone edge fixed origin Regional Tiered Cache A regional hop absorbs misses before the upper tier sees them edge region upper origin
Each topology trades a different amount of added latency for a different reduction in origin fetches.
Topology Origin offload on a fresh deploy First-byte latency at a cold PoP Purge propagation Plan
No Tiered Cache None — one origin fetch per PoP with traffic Lowest hop count, highest absolute latency Broadcast to every PoP at once All
Smart Tiered Cache High — typically one origin fetch per object One extra intra-network hop Broadcast to lower and upper tier together Pro and above
Generic Global High, but routed through a data centre you chose Extra hop, length depends on your choice Same broadcast Enterprise
Regional Tiered Cache Highest — regional tiers absorb misses first Two extra hops for a fully cold object Same broadcast, both tiers Enterprise

Smart Tiered Cache is the correct default for almost every fingerprinted-asset workload. It picks the upper tier using Cloudflare’s own latency measurements between data centres and your origin, and it re-picks when network conditions change. Generic Global earns its place when your origin is genuinely single-homed and you know the best transit path better than the automatic selection does — a single origin in São Paulo, for example, where nominating a nearby upper tier beats a globally-optimised guess. Regional Tiered Cache is for origins that cannot absorb a global miss storm at all: it adds a per-region layer so the upper tier itself sees a fraction of the misses.

Enabling It

Tiered Cache is a zone-level setting rather than a Cache Rule, but the Cache Rule that marks your assets as long-lived is what makes it worth enabling. Both calls belong in the same infrastructure script:

#!/usr/bin/env bash
# enable-tiered-cache.sh — topology plus the Cache Rule that makes it useful
set -euo pipefail

ZONE_ID="${CF_ZONE_ID:?CF_ZONE_ID must be set}"
API_TOKEN="${CF_API_TOKEN:?CF_API_TOKEN must be set}"
API="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}"

# 1. Turn on Argo Tiered Cache for the zone
curl -s -X PATCH "${API}/argo/tiered_caching" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"value":"on"}'

# 2. Select the Smart topology (the automatic upper-tier selection)
curl -s -X PATCH "${API}/cache/tiered_cache_smart_topology_enable" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"value":"on"}'

# 3. Make hashed assets eligible for a long upper-tier TTL.
#    Without a long edge_ttl the upper tier evicts as fast as the edge does
#    and the shield stops shielding anything.
curl -s -X POST \
  "${API}/rulesets/phases/http_request_cache_settings/entrypoint/rules" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "description": "Tiered-cache friendly TTL for fingerprinted assets",
    "expression": "(http.request.uri.path matches \"^/assets/.*\\\\.[a-f0-9]{8,16}\\\\.(js|css|woff2|png|webp|svg)$\")",
    "action": "set_cache_settings",
    "action_parameters": {
      "cache": true,
      "edge_ttl": { "mode": "override_origin", "default": 31536000 },
      "browser_ttl": { "mode": "override_origin", "default": 31536000 },
      "serve_stale": { "disable_stale_while_updating": false }
    }
  }'

The 8-hex-character hash in that expression is the default emitted by Webpack, Vite, Rollup, and esbuild; widen it to 12–16 characters if you run a monorepo producing thousands of chunks and want extra collision headroom.

Why Immutability Makes the Topology Nearly Free

Every extra cache layer normally costs you two things: a consistency risk and a latency tax on revalidation. Fingerprinted assets pay neither.

There is no consistency risk because the URL is a function of the content. The upper tier cannot serve a stale version of app.a1b2c3d4.js — a “stale” copy would have to be different bytes, which would have produced a different filename. Two tiers holding the same URL therefore hold identical objects by construction, forever.

There is no revalidation tax because an object served with Cache-Control: public, max-age=31536000, immutable is never conditionally re-checked within its lifetime. A conventional CDN setup burns a round trip per tier every time a TTL lapses; with a year-long immutable TTL those round trips simply never happen. The immutable and TTL tuning guide works through the header combinations that make this hold in practice, including the stale-while-revalidate variants you want on HTML but not on assets.

The remaining cost is the one hop on a cold miss, which is what the next section measures.

The Cost: First-Byte Latency at a Cold PoP

Every PoP pays a one-time penalty the first time a user there asks for a new hash. The size of that penalty is the whole argument for or against the extra tier.

Cold-cache latency by layer Bar chart of first-byte latency: twelve milliseconds when the object is already at the edge, forty-eight when the upper tier answers, and three hundred and ten when the request reaches the origin. First-byte latency for one fingerprinted asset Edge HIT object already at this PoP 12 ms Upper-tier HIT one hop inside the network 48 ms Origin fetch cold at both tiers 310 ms Each PoP pays the middle row once; only one PoP pays the bottom row
The upper tier converts a transcontinental origin fetch into an intra-network hop for every PoP except the first.

Read the arithmetic across a whole release rather than a single request. Without a tier, a hundred PoPs each pay 310 ms on their first request — a hundred slow requests, plus a hundred origin connections. With a tier, one PoP pays 310 ms and the other ninety-nine pay 48 ms. Total user-visible slow time drops by roughly 85%, and the origin sees a single connection per object. That is why tiered topology is close to free for immutable assets and genuinely contentious for dynamic ones: the dynamic case adds a hop to every miss and misses are frequent, whereas a fingerprinted asset misses exactly once per PoP for its entire lifetime.

How a Purge Moves Through the Tiers

Cloudflare broadcasts purge instructions to lower and upper tiers as one operation, so the tiers do not need to be purged in sequence and there is no window where a lower tier repopulates itself from a stale upper tier. In practice both layers drop the object within a couple of seconds of the API call returning.

The ordering nuance that does bite: the upper tier is now the first thing a purged lower tier talks to. Purge one HTML URL and the next request in each city produces a lower-tier miss, all of which arrive at the upper tier at once. The upper tier itself missed too, so it makes one origin fetch and fans the answer back out. Your origin sees a single request where it might otherwise have seen dozens — the shield works during a purge exactly as it does during a deploy. Teams running more than one CDN lose that property and have to reason about each network’s shield separately, which is the subject of the multi-CDN and origin shield purging guide.

Interaction with Cache Reserve

Cache Reserve sits behind the upper tier, not beside it. The lookup order on a cold request is lower tier, upper tier, Cache Reserve, origin. That ordering has one consequence worth planning around: an asset that has aged out of both tiers is still served from Cache Reserve without touching your origin, so the bottom row of the latency chart above becomes rare rather than merely infrequent.

For a fingerprinted bundle this matters most for rarely requested chunks. A chunk requested twice a week in Auckland will fall out of the Auckland edge cache and eventually out of the upper tier too, and without Cache Reserve every one of those requests is an origin fetch months after the deploy. With it, the object persists for its full declared TTL. The trade-off is that Cache Reserve is billed on storage and operations, so enable it for the hashed-asset path and leave HTML — which you are purging anyway — out of it.

Verification: Compare Two PoPs

The check that proves the topology is live is a paired request from two different regions against the same URL. CF-Cache-Status tells you whether the edge had it; Age tells you whether the copy it served came from somewhere that had it earlier.

Cloudflare’s addresses are anycast, so you cannot pick a PoP with --resolve or a different DNS resolver — the PoP is chosen by where the packets enter the network. Run the identical script from two hosts in different regions (a CI runner and a laptop, or two small VPS instances) and compare the output:

#!/usr/bin/env bash
# verify-tiered-cache.sh — run from two hosts in different regions, compare output
set -euo pipefail

URL="${1:?usage: verify-tiered-cache.sh <asset-url>}"

# Force a fresh connection so a keep-alive session cannot reuse an earlier PoP
curl -sI --no-keepalive "${URL}" \
  | grep -iE "^(cf-cache-status|age|cf-ray|cache-control):"

Invoke it as ./verify-tiered-cache.sh https://www.example.com/assets/app.a1b2c3d4.js on each host. cf-ray ends in the three-letter code of the data centre that served the response, so you can confirm the two hosts really did land on different PoPs. A healthy tiered zone looks like this on the very first request from a cold city:

cf-cache-status: MISS
age: 0
cf-ray: 8a1f2c3d4e5f6789-WAW
cache-control: public, max-age=31536000, immutable

and like this from a second cold city moments later:

cf-cache-status: MISS
age: 37
cf-ray: 8a1f2c3d4e5f6790-DEN
cache-control: public, max-age=31536000, immutable

The second response is the proof. MISS means Denver’s edge did not have the object; age: 37 means the copy it handed over had already been alive for 37 seconds somewhere else — the upper tier, seeded by Warsaw. Without Tiered Cache that second response would read age: 0, because Denver would have fetched fresh bytes straight from your origin.

Run the same pair again a few seconds later and both should return HIT with a growing Age. If the second city keeps reporting age: 0 on repeated cold URLs, the topology is not in effect: check that the zone-level setting is on, and that no Cache Rule on the asset path sets an edge TTL short enough for the upper tier to evict between requests.

When to Reconsider

Skip Tiered Cache when your origin is itself a global network. If assets are served from an object store with edge replication, the upper tier adds a hop to reach something that was already close to every PoP. Measure before assuming: an origin fetch that costs 40 ms globally makes the shield pointless.

Prefer Regional over Smart when the origin is capacity-bound, not latency-bound. A single application server that falls over at a few hundred concurrent connections benefits more from the extra absorption layer than from optimal routing, even though Regional adds a hop.

Turn it off for genuinely per-user content. Personalised responses miss on every request by design, so a second tier is pure added latency. Scope Tiered Cache benefits to the asset paths and accept that the dynamic routes gain nothing — the reason the Cloudflare Cache Rules and purge guide recommends separating asset and HTML rules in the first place.

FAQ

Does Tiered Cache delay how fast a new deploy reaches users?

No. Users never wait for propagation because there is none — the first request in each city pulls the object through. Tiered Cache changes where that pull terminates, making it faster for every city after the first.

Will the upper tier ever serve an old version of a hashed file?

It cannot. The filename is derived from the bytes, so a different version is a different URL. This is the property that makes multiple cache layers safe without any coordination between them.

Do I need Argo Smart Routing to use Tiered Cache?

No. Tiered Cache is enabled through the Argo API namespace but is a separate, separately-priced feature. Smart Routing optimises the path to your origin; Tiered Cache reduces how often that path is used at all.

Why does CF-Cache-Status never say the upper tier answered?

The header reports the lower tier’s own lookup result, and from its perspective the object was not in local cache. Cloudflare exposes no dedicated upper-tier status value, which is why the Age comparison across two PoPs is the practical test.