sellIntegration
cached

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.

330+Edge cities that cache
2Loads to go MISS then HIT
0Origin trips on a cache HIT
5cf-cache-status values to know
insights

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.

schemaThe cache decision (one request)

Yes

No

Yes

No

Request hits the edge

In cache and still fresh?

Serve from cache (HIT)

Fetch from origin / Worker

Cacheable?

Store per Cache-Control / TTL

Serve uncached (DYNAMIC / BYPASS)

Serve to visitor (MISS)

kitchen

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).

school

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.

hub

Edge cache

Cloudflare's nearby server that stores a copy of your response so it can answer without bothering your origin.

dns

Origin

Your real server or Worker that produces the original response. The edge only goes here on a MISS or to revalidate.

timer

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.

schedule

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.

mop

Purge / invalidation

Manually evicting a cached copy now, before its TTL ends — by URL, tag, or everything — so the next request rebuilds it.

fingerprint

Cache key

What the edge uses to look a copy up — by default the full URL. Two different URLs are two different cache entries.

swap_vert

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).

schemaThree requests, three outcomes
"Origin / Worker""Edge cache""Browser""Origin / Worker""Edge cache""Browser"First visit, nothing cachedWithin TTL, copy is freshAfter TTL, copy is staleGET /logo.pngCache empty, fetch from origin200 plus Cache-Control max-agecf-cache-status MISSGET /logo.pngcf-cache-status HITGET /logo.pngRevalidate with If-None-Match304 Not Modifiedcf-cache-status REVALIDATED
verified

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.

timeline

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.

schemaThe life of one cached copy

Stored (t = 0)

FRESH within max-age

TTL reached

STALE past max-age

Next request triggers revalidate

304: keep copy, reset to FRESH

200: store new copy, reset to FRESH

bolt

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.

fact_check

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.

check_circle

HIT

A fresh copy was in the edge cache and served instantly. No origin trip — the fastest, cheapest result.

cancel

MISS

Nothing cached for this key, so the edge fetched from origin and, if allowed, stored it for next time.

history

EXPIRED

A copy existed but its TTL ran out, so the edge revalidated or refetched a fresh version from origin.

auto_mode

DYNAMIC

The content was treated as dynamic and not eligible to cache by default — every request goes to origin.

block

BYPASS

Caching was deliberately skipped — by a Cache Rule, a no-store header, or a setting telling the edge not to cache.

verified

REVALIDATED

The stale copy was checked against origin (ETag / If-None-Match), origin replied 304, and the same copy was reused.

construction

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.

httpCache-Control header recipes
# 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-store
jsworker.js — set Cache-Control
export 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"
      }
    });
  }
};
  1. Send the header

    Deploy your Worker or configure your origin so cacheable responses carry a public Cache-Control with an s-maxage.

  2. Request it twice

    Run curl -sI twice. The first is usually MISS (cold), the second should be HIT (warm) from the same edge city.

    bash
    # -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: HIT
  3. Purge after an update

    Changed the file before its TTL ends? Purge it so the next request rebuilds a fresh copy.

    bash
    # 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"]}'
compare_arrows

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.

schemaWhich cache catches the request

Yes

No

Yes

No

Incoming request

Static and same for everyone?

CDN edge cache: automatic, URL-keyed

Worker logic runs

Reusable computed data?

Workers KV: read/write by key in code

Origin / database

public

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.

key

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.

auto_awesome

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.

layers

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.

tips_and_updates

Gotchas & tips

lock_person

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.