How caching works (hit, miss, revalidate)
See exactly when the edge answers instantly, when it calls your origin, and when it refreshes a stale copy.
What we are visualizing
Every time a browser asks Cloudflare for a file, an edge node runs the same quick decision: do I already have a fresh copy? If yes, it answers in milliseconds (a cache HIT). If no, it fetches from your origin server or Worker, optionally stores the response, then serves it (a cache MISS). This guide draws that whole lifecycle as diagrams so you can picture it.
Think of it like a fridge
The edge cache is your fridge; the origin is the supermarket. If the milk is in the fridge and not past its date (TTL), you drink it right away (HIT). If it is gone, you go to the shop (MISS). If it is just past the date, you sniff-check before buying new (revalidate).
The words on the diagrams
Before reading the flows, here are the five terms that show up again and again. Each one maps to a box or arrow you will see in the diagrams below.
Edge cache
Cloudflare's nearby server that stores a copy of your response so it can answer without bothering your origin.
Origin
Your real server or Worker that produces the original response. The edge only goes here on a MISS or to revalidate.
TTL
Time To Live — how many seconds a cached copy counts as fresh. Set by Cache-Control max-age / s-maxage or a Cache Rule.
Fresh vs stale
Inside its TTL a copy is fresh and served instantly. Past the TTL it turns stale and must be revalidated or refetched.
Purge / invalidation
Manually evicting a cached copy now, before its TTL ends — by URL, tag, or everything — so the next request rebuilds it.
Cache key
What the edge uses to look a copy up — by default the full URL. Two different URLs are two different cache entries.
HIT vs MISS vs revalidate
The same URL behaves differently depending on what the edge already holds. This sequence shows the same /logo.png requested three times: first cold (MISS), then warm (HIT), then after the TTL expires (revalidate).
Revalidate saves bandwidth
On revalidate, the edge asks 'has this changed?' with a validator (ETag / Last-Modified). A 304 Not Modified means it can keep the copy it already has and only refresh the timer — no full re-download.
TTL over time: fresh, stale, revalidate
A cached copy has a clock on it. From the moment it is stored, it stays FRESH until max-age runs out, then becomes STALE. The next request after that is what triggers a revalidation, and the result resets the clock back to FRESH.
stale-while-revalidate
With Cache-Control: max-age=60, stale-while-revalidate=600 the edge can instantly serve the stale copy to the user while refreshing it from origin in the background. Visitors never wait for the revalidation.
Reading cf-cache-status
Cloudflare tells you which branch of the diagram a request took with a response header named cf-cache-status. These are the values you will meet most often.
HIT
A fresh copy was in the edge cache and served instantly. No origin trip — the fastest, cheapest result.
MISS
Nothing cached for this key, so the edge fetched from origin and, if allowed, stored it for next time.
EXPIRED
A copy existed but its TTL ran out, so the edge revalidated or refetched a fresh version from origin.
DYNAMIC
The content was treated as dynamic and not eligible to cache by default — every request goes to origin.
BYPASS
Caching was deliberately skipped — by a Cache Rule, a no-store header, or a setting telling the edge not to cache.
REVALIDATED
The stale copy was checked against origin (ETag / If-None-Match), origin replied 304, and the same copy was reused.
Control it: headers + verify
You steer all of the above with one HTTP header your origin or Worker sends: Cache-Control. max-age is the browser TTL; s-maxage is the edge (shared cache) TTL and overrides max-age at Cloudflare.
# Static asset: 1 hour in browsers, 1 day on the edge
Cache-Control: public, max-age=3600, s-maxage=86400
# Serve stale instantly, refresh in the background for 10 min
Cache-Control: public, max-age=60, stale-while-revalidate=600
# Per-user / logged-in page: never cache anywhere
Cache-Control: private, no-storeexport default {
async fetch(request) {
const body = JSON.stringify({ hello: "world" });
return new Response(body, {
headers: {
"Content-Type": "application/json",
// Browser keeps it 1h; Cloudflare edge keeps it 1 day
"Cache-Control": "public, max-age=3600, s-maxage=86400"
}
});
}
};Send the header
Deploy your Worker or configure your origin so cacheable responses carry a public Cache-Control with an s-maxage.
Request it twice
Run curl -sI twice. The first is usually MISS (cold), the second should be HIT (warm) from the same edge city.
# -s silent, -I headers only; look at cf-cache-status curl -sI https://example.com/logo.png | grep -i cf-cache-status # 1st run -> cf-cache-status: MISS # 2nd run -> cf-cache-status: HITPurge after an update
Changed the file before its TTL ends? Purge it so the next request rebuilds a fresh copy.
# Purge a single URL via the API curl -X POST \ "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CF_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"files":["https://example.com/logo.png"]}'
CDN cache vs KV cache
People say 'cache' for two very different things on Cloudflare. The CDN cache (this guide) is automatic and keyed by URL. Workers KV is an application cache you read and write by key from your own code. They solve different problems.
CDN cache
Caches whole HTTP responses, keyed by URL, shared across all users. Controlled by Cache-Control and Cache Rules. Great for assets and pages.
KV cache
You store arbitrary values under keys you choose, read/written in Worker code. Good for computed API results, config, and per-key data.
Automatic vs manual
CDN caching just happens once content is proxied. KV does nothing until your code calls KV.get and KV.put — you own the logic.
Use both together
A Worker can read from KV to build a response, then send Cache-Control so the CDN caches that response at the edge — two layers, one fast path.
Gotchas & tips
Never cache per-user pages
If a logged-in dashboard gets cached publicly, one user can see another user's data. Always send Cache-Control: private, no-store for personalized or authenticated responses.
- A 200 with no Cache-Control often becomes DYNAMIC — set the header to make it cacheable.
- cf-cache-status is per edge location, so a fresh MISS in a new city is normal.
- Version your filenames (app.v2.js) so updates create a new cache key instead of needing a purge.
- Query strings change the cache key by default — /a?x=1 and /a?x=2 are cached separately.
- Use stale-while-revalidate so users never wait on a revalidation round-trip.
Related products
menu_bookOfficial docsopen_in_new