Configuring Tiered Cache on Cloudflare
This guide is part of Multi-Tier CDN Cache Architecture. It walks through enabling Cloudflare Tiered Cache and Cache Reserve, configuring them through the dashboard and the API, and verifying that origin traffic actually dropped.
The problem
Your Cloudflare zone caches at the edge, but origin load is still high. Each Cloudflare data center maintains its own cache, so a popular object that expires or lands in a cold data center triggers an independent origin fetch from every PoP. With requests arriving worldwide, the same object is fetched from origin dozens of times. You want lower-tier data centers to consult an upper tier — and ultimately a single origin shield — before ever touching origin, and you want a persistent tier so infrequently requested objects survive normal eviction.
Root cause: independent PoP caches
By default, Cloudflare’s data centers do not share cache state. A miss in any data center goes straight to origin. Origin request volume therefore scales with the number of cold data centers, not with the number of unique objects. Tiered Cache changes the topology: it designates upper-tier data centers that lower tiers treat as their upstream, so most misses are absorbed before origin. Cache Reserve adds a durable, R2-backed tier that holds objects far longer than the regular eviction policy allows, raising the offload ratio for content that is requested infrequently but repeatedly.
Step 1: Confirm prerequisites
- The zone is active on Cloudflare and proxied (orange-cloud) for the hostnames you want to cache.
- Responses are cacheable: a
Cache-Controlwiths-maxageormax-ageand noSet-Cookieon cacheable paths. Tiering cannot help an uncacheable response. - You have an API token scoped to Zone → Cache Settings → Edit if configuring via API.
Step 2: Enable Tiered Cache
In the dashboard, open Caching → Tiered Cache and enable Smart Tiered Cache Topology. Smart topology lets Cloudflare pick a single upper tier automatically, which is the right default for most origins.
To do it via the API, set the tiered_cache_smart_topology_enable setting and turn on Argo tiered caching:
# Enable Smart Tiered Cache topology
curl -X PATCH \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/tiered_cache_smart_topology_enable" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"value":"on"}'
# Ensure tiered caching itself is on
curl -X PATCH \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/argo/tiered_caching" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"value":"on"}'
A successful response has "success": true. Smart topology consolidates upper-tier traffic toward a single data center near your origin, effectively giving you an origin shield without manually mapping regions.
Step 3: Enable Cache Reserve
Cache Reserve adds a persistent R2-backed tier. Enable it under Caching → Cache Reserve in the dashboard, or via API:
curl -X PATCH \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"value":"on"}'
Cache Reserve only stores objects that are eligible: they must have a Cache-Control that makes them cacheable and, in practice, a sufficiently long freshness lifetime. It bills for storage and operations, so scope it to content classes that benefit from long retention (static assets, media, infrequently changing API responses).
Step 4: Make responses Cache-Reserve eligible
Set explicit cache headers from origin or from a Worker so objects qualify for the persistent tier. Use s-maxage to control the shared tiers independently of the browser:
export default {
async fetch(request: Request): Promise<Response> {
const origin = await fetch(request, { cf: { cacheEverything: true } });
const headers = new Headers(origin.headers);
headers.delete("set-cookie"); // a Set-Cookie makes the object uncacheable in shared tiers
headers.set("cache-control", "public, max-age=300, s-maxage=86400");
return new Response(origin.body, {
status: origin.status,
headers,
});
},
};
cf: { cacheEverything: true } instructs Cloudflare to cache the response regardless of file extension; the long s-maxage gives the upper tiers and Cache Reserve a generous lifetime while keeping browser caching short.
Step 5: Validate
Request the same URL twice and inspect cf-cache-status:
curl -sI "https://www.example.com/assets/app.js" | grep -i cf-cache-status
# First request: cf-cache-status: MISS
# Second request: cf-cache-status: HIT
To confirm tiering specifically, request the object from a region far from your origin and from one near it; the distant region should report HIT shortly after the near region populated the upper tier, rather than each region producing its own MISS. The values you will see:
MISS— not in cache; fetched from the next tier or origin.HIT— served from cache.EXPIRED— found but stale; revalidated.REVALIDATED/UPDATING— served while refreshing.DYNAMIC— not cacheable (check forSet-Cookieor missingCache-Control).
A DYNAMIC status on something you expect to cache is the most common misconfiguration — it almost always means a Set-Cookie header or absent cache directive.
Local vs production divergence
| Behavior | wrangler dev / local |
Production zone |
|---|---|---|
| Tiered Cache topology | Not emulated; single local cache | Upper tiers consolidate misses |
| Cache Reserve | Not available locally | Persistent R2-backed tier |
cf-cache-status header |
Absent or DYNAMIC |
Full HIT/MISS/EXPIRED lifecycle |
cf request options |
Largely ignored | Honored |
Validate cacheability (headers, no stray cookies) locally, but verify tiering and Cache Reserve only against a real zone — ideally a staging zone before production.
Validation with Vitest
You cannot test Cloudflare’s topology in unit tests, but you can assert your Worker emits Cache-Reserve-eligible headers and strips cookies:
import { env, createExecutionContext } from "cloudflare:test";
import { describe, it, expect, vi } from "vitest";
import worker from "./index";
describe("tiered-cache header shaping", () => {
it("sets s-maxage and removes Set-Cookie", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("body", { headers: { "set-cookie": "sid=1", "content-type": "text/plain" } }),
);
const ctx = createExecutionContext();
const res = await worker.fetch(new Request("https://www.example.com/a.js"), env, ctx);
expect(res.headers.get("set-cookie")).toBeNull();
expect(res.headers.get("cache-control")).toContain("s-maxage=86400");
});
});
Pitfalls
cf-cache-status: DYNAMICeverywhere. ASet-Cookieheader or a missingCache-Controlmakes the object uncacheable. Strip cookies and set explicit directives.- Cache Reserve stores nothing. Objects with short or absent freshness lifetimes are ineligible. Give them a long
s-maxage. - Tiering enabled but origin load unchanged. The zone may not be proxied, or responses vary on a header/cookie that fragments the key. Confirm orange-cloud and minimize
Vary. - Unexpected Cache Reserve bill. It bills for storage and operations; enabling it zone-wide on high-churn content is wasteful. Scope it to long-lived assets.
- Stale content after publish. Cache Reserve is persistent, so TTL expiry alone is slow to clear it. Purge by tag or URL to evict it deliberately.
Production deployment checklist
- Responses set
s-maxageand carry noSet-Cookie -
cf-cache-status
Frequently Asked Questions
What is the difference between Tiered Cache and Cache Reserve?
Tiered Cache changes the routing topology so lower-tier data centers consult an upper tier before origin, reducing duplicate origin fetches. Cache Reserve adds a persistent, R2-backed storage tier that holds objects far longer than normal eviction allows. Tiered Cache reduces how often origin is contacted; Cache Reserve keeps infrequently requested objects available so they are not re-fetched after eviction.
Why does cf-cache-status show DYNAMIC for a file I want cached?
DYNAMIC means Cloudflare considers the response uncacheable. The usual causes are a Set-Cookie header on the response or a missing or non-public Cache-Control directive. Strip the cookie and set Cache-Control: public, s-maxage=... so the object becomes eligible.
Does Smart Tiered Cache give me an origin shield?
Effectively yes. Smart Tiered Cache Topology consolidates upper-tier traffic toward a single data center close to your origin, so most misses funnel through one upstream before reaching origin. That single-funnel behavior is the origin-shield pattern, configured automatically rather than by hand.
How do I clear an object from Cache Reserve after publishing?
Because Cache Reserve is persistent, TTL expiry alone clears it slowly. Issue an explicit purge — by URL or, at scale, by cache tag — so the object is evicted from every tier including the reserve. Then pre-warm the URL so the tiers repopulate before peak traffic.