---
title: "cf-cache-status explained: every Cloudflare cache status and how to fix it — warmup.rocks"
description: "What cf-cache-status HIT, MISS, DYNAMIC, BYPASS, EXPIRED, STALE, REVALIDATED and UPDATING mean, why your HTML shows DYNAMIC, and how to turn every status into a HIT."
canonical: https://warmup.rocks/blog/cf-cache-status
---

Cloudflare

# cf-cache-status explained: every value, what it costs you, and how to fix it

July 2026 · 8 min read · [← All posts](https://warmup.rocks/blog/)

`cf-cache-status` is the single most informative header Cloudflare attaches to a response. One word tells you whether the request was answered in milliseconds from a data center near the visitor — or traveled all the way to your origin server and back. This guide explains every value, the difference between the confusing ones (`DYNAMIC` vs. `BYPASS`, `MISS` vs. `EXPIRED`), and the concrete fix for each.

## How to check it

Three ways, from quick to thorough:

-   **DevTools:** Network tab → click the request → Response Headers → `cf-cache-status`.
-   **curl:**
    
    ```
    curl -sI https://example.com/ | grep -iE 'cf-cache-status|cf-ray|age'
    ```
    
    The `cf-ray` suffix (e.g. `…-FRA`) tells you _which data center_ answered — important, because each one has its own cache. (Decode any code in our [list of all Cloudflare data centers](https://warmup.rocks/cloudflare-data-centers).)
-   **Multi-location check:** a HIT from your desk says nothing about the 280+ other Cloudflare colos. Our free [cache checker](https://warmup.rocks/cache-checker) requests your URL from six locations on four continents simultaneously and shows the status per location.

## Every cf-cache-status value

| Status | What happened | Cost to the visitor |
| --- | --- | --- |
| `HIT` | Served from the edge cache. Origin never contacted. | ✅ None — this is the goal |
| `MISS` | Cacheable, but not in this colo's cache. Fetched from origin, stored for next time. | ⚠️ Full origin round trip |
| `EXPIRED` | Was in cache, but the edge TTL ran out. Re-fetched from origin. | ⚠️ Full origin round trip |
| `STALE` | Served the expired copy while fetching a fresh one in the background. | ✅ Fast, content slightly old |
| `UPDATING` | Like STALE, but the refresh is already in flight (popular objects). | ✅ Fast |
| `REVALIDATED` | Expired entry checked against origin — still fresh (304), served from cache. | ✅ Cheap origin ping only |
| `DYNAMIC` | Not eligible for caching under default rules. Every request goes to origin. | ❌ Never cached |
| `BYPASS` | Eligible, but explicitly opted out (`no-store`, `private`, cookie, bypass rule). | ❌ Never cached |
| `NONE / UNKNOWN` | No cache lookup happened — Workers subrequests, redirects, errors. | — depends |
| _(header absent)_ | The response didn't pass through Cloudflare's cache at all. | ❌ Check your DNS/proxy status |

## DYNAMIC: the one that surprises everyone

Paste any normal web page into a header checker and you'll almost certainly see `DYNAMIC`. That's not a bug — it's Cloudflare's default: **only static file extensions are cached** (images, CSS, JS, fonts, PDFs, …). **HTML is not on the list.** Your carefully tuned origin cache headers don't change this; Cloudflare ignores them for ineligible content types.

Two ways to make HTML cacheable:

1.  **Cache Rule** (dashboard → Caching → Cache Rules): match your hostname or path, set "Eligible for cache", and choose an edge TTL. Exclude logged-in traffic by cookie:
    
    ```
    (http.host eq "example.com"
     and not http.cookie contains "session")
    ```
    
2.  **`Cloudflare-CDN-Cache-Control` header** from your origin — it instructs Cloudflare's cache specifically, without affecting browsers:
    
    ```
    Cloudflare-CDN-Cache-Control: max-age=14400
    Cache-Control: max-age=0, must-revalidate
    ```
    

The full walkthrough, including Workers Cache and APO, is in our [Cloudflare cache warming guide](https://warmup.rocks/blog/cloudflare-cache-warming).

## BYPASS: cacheable, but opted out

`BYPASS` looks similar to `DYNAMIC` but has the opposite cause: the content _was_ eligible, and something explicitly told Cloudflare not to cache this response. The usual suspects:

-   `Cache-Control: no-store`, `no-cache` or `private` from your origin,
-   a `Set-Cookie` header on the response (very common: a framework starting sessions on every request),
-   a Cache Rule or Page Rule set to "Bypass cache".

Fix the origin header or the stray cookie and `BYPASS` becomes `MISS` on the next request — then `HIT` after that.

## MISS and EXPIRED: the geography problem

A `MISS` isn't a configuration error — it's the price of a cold cache. What most people underestimate is _how often_ caches are cold:

-   **Per location:** Cloudflare runs 280+ data centers, and each caches independently. A HIT in Frankfurt is meaningless for the visitor routed to Singapore. (Verify it yourself with the [multi-location checker](https://warmup.rocks/cache-checker).)
-   **Per TTL:** every entry expires after the edge TTL and the next request is `EXPIRED` — economically identical to a MISS.
-   **Per purge:** every deploy with "purge everything" resets all locations to cold at once.
-   **Per eviction:** unpopular objects get evicted long before their TTL to make room — the long tail of your site is cold almost everywhere, almost always.

Organic traffic only fixes this for your most popular pages in your busiest regions. For everything else, the first visitor in each region pays the origin round trip — unless you fill the caches proactively. That's what [warmup.rocks](https://warmup.rocks/) does: it requests every URL in your sitemap from 90+ edge locations on a schedule, so the cache is already warm when a real visitor arrives, and shows you the resulting [hit ratio](https://warmup.rocks/blog/cache-hit-ratio) per location after every run.

## STALE, UPDATING, REVALIDATED: the good expired states

These three mean your configuration is working _well_: instead of making a visitor wait for origin, Cloudflare served the slightly-old copy (`STALE`, `UPDATING`) or confirmed freshness with a cheap 304 (`REVALIDATED`). You get them by sending validators (`ETag` / `Last-Modified`) and, on Enterprise, `stale-while-revalidate` directives. No action needed — count them as hits in your monitoring.

## Debug checklist

1.  **No `cf-cache-status` at all?** The hostname isn't proxied (grey cloud in DNS) or the request bypasses Cloudflare entirely.
2.  **`DYNAMIC` on HTML?** Expected default — add a Cache Rule or `Cloudflare-CDN-Cache-Control`.
3.  **`BYPASS`?** Find the `no-store`/`private` directive or the `Set-Cookie` on the response.
4.  **`MISS` that never becomes `HIT`?** Check `cf-ray` — you're probably hitting different colos per request. Test twice against the same colo before concluding anything.
5.  **`HIT` here, slow for users elsewhere?** Run the [multi-location check](https://warmup.rocks/cache-checker) — other regions are likely cold, and [cache warming](https://warmup.rocks/blog/what-is-cache-warming) is the fix.
