How to check if CDN cache is hit or miss
Print the headers and keep the CDN's own status line: curl -s -D - -o /dev/null <url> | grep -i -E 'x-cache|cf-cache-status|age'. One request tells you what happened to it. Two requests to the same cold URL tell you whether the second one was served warm.
Why check this
A cache that never warms turns every visitor into an origin request, and the symptom is a slow page with a healthy-looking origin. Run this after a CDN rule change, after adding a query parameter to a URL, and on any release that alters vary or cookies. The failure it catches is a page that reports MISS on every request because a Set-Cookie or a request-specific header makes the response uncacheable, so the origin absorbs all the traffic the CDN was bought to absorb.
Prerequisites
- curl 7.0 or later. Any build reads headers. See the curl manual.
- A URL nobody has requested recently. Adding an unused query parameter creates one, because caches key on the full URL.
- The
ageheader is defined in RFC 9111 section 5.1. The status header name is not standard and differs per vendor. - The value set is vendor specific too. Cloudflare documents cf-cache-status and Fastly documents its x-cache values.
Steps
- Step 1.
Read the status header and
agetogether. A hit that has been stored for a while shows both.curl -s -D - -o /dev/null 'https://www.cloudflare.com/' | grep -i -E '^cf-cache-status|^age|^cache-control'cf-cache-status: HIT age: 3 cache-control: max-age=10age: 3againstmax-age=10says the stored copy is 3 seconds old with 7 seconds of freshness left. - Step 2.
Request a cold URL. An unused query parameter gives the edge a key it has never seen.
curl -s -D - -o /dev/null 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control?probe=1' | grep -i -E '^x-cache|^x-cache-hits|^age'age: 0 x-cache: MISS, MISS, MISS x-cache-hits: 0, 0, 0Three values, three cache layers, read in request order. Every one of them missed, and
age: 0confirms the response was produced now. - Step 3.
Repeat the identical request and compare. The layer that stored the response on the way back now answers from storage.
curl -s -D - -o /dev/null 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control?probe=1' | grep -i -E '^x-cache|^x-cache-hits|^age'age: 0 x-cache: MISS, MISS, HIT x-cache-hits: 0, 0, 1The third position flipped to
HITand its hit counter reads 1. The two layers in front still missed, which is why one word is never the whole answer. - Step 4.
Check a host you did not expect to be behind a cache. The status header appears wherever an edge is in the path.
curl -s -D - -o /dev/null 'https://example.com/' | grep -i -E '^cf-cache-status|^age|^server'server: cloudflare age: 7386 cf-cache-status: HITAn
ageof 7386 seconds on a document with nocache-controlheader at all. The edge chose that lifetime by heuristic, and nobody configured it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| HIT with age above 0 | Served from the edge, no origin request | Nothing. Compare age against max-age to see how much freshness is left. |
| MISS on the first request, HIT on the second | The cache warms correctly | Working. Record which layer flipped, as in step 3. |
| MISS on every request | The response is not being stored | Look at set-cookie, vary and cache-control on the response. Any of the three can block storage. |
| A value naming a bypass, such as DYNAMIC | The edge decided this route is not cacheable | Check the CDN rule before the headers. A path rule outranks the origin's directives. |
| A value naming a revalidation, such as REVALIDATED | The entry existed and was rechecked with the origin | Cheap if a validator exists, a full transfer if not. Check the ETag. |
| age present, status header absent | A cache is in the path and does not name itself | Trust age. It is the standard header, and it is enough to prove storage. |
Common mistakes
What to check next
- How to check if a site uses a CDN: work out which vendor sends the status header you are reading.
- How to check cache-control header: the directives the edge obeys when it decides to store or skip.
- How to check ETag header: what turns a revalidation into a 304 instead of a full transfer.
- How to check stale-while-revalidate: why
agecan exceedmax-agewhile the status still reads as served from cache.
FAQ
What is the difference between cache hit and cache miss?
A hit is answered from the cache with no origin request. A miss goes to the origin and is usually stored on the way back, which is why the second identical request in step 3 hits. Both return 200 to the client.
How to check x-cache header?
Filter it out of the response headers, as in step 2. The value can be a list when several caches are chained, read left to right in request order, with x-cache-hits giving the per-layer counters.
How to check cf-cache-status header?
The same command with a different header name, as in step 1. Cloudflare uses cf-cache-status, Fastly and Varnish use x-cache, and other vendors pick their own. Grep for all of them at once.
How to check age header?
age counts the seconds the response has sat in a cache. It is standard, so it appears whatever the vendor. Step 4 shows an age of 7386 on a document whose origin sends no cache-control at all.
Does a HIT mean the content is current?
No. It means the content came from storage. Compare age with max-age to see whether it is still fresh, and read the stale window before assuming an old age is a fault.
Verified
Verified by Maks Vernycurl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic5 minpublished updated Maks Verny