# Cache status headers

> Read X-Cache-Status, the standard Cache-Status header and Age to see whether the edge served a response from cache and why not.

Every response of a name with the cache on carries two headers that say what the cache did, plus `Age` when it served a stored copy.

```sh
curl -sI https://www.example.com/app.css | grep -i -E 'x-cache-status|cache-status|age'
```

## X-Cache-Status

Uses the values you may know from other CDNs:

| Value | Meaning |
|---|---|
| `HIT` | Served from the edge cache. |
| `MISS` | Eligible but not cached yet: fetched from the origin and stored when allowed. |
| `EXPIRED` | The stored copy had expired; a fresh one came from the origin. |
| `REVALIDATED` | The stored copy had expired; the origin confirmed it with `304`. |
| `UPDATING` | An expired copy served while it refreshes in the background (stale-while-revalidate). |
| `STALE` | An expired copy served because the origin failed. |
| `BYPASS` | Eligible, but the response couldn't be stored (for example `Set-Cookie` or `private`). |
| `DYNAMIC` | Not eligible: the cache wasn't looked up (HTML without a rule, a `bypass` rule, a `POST`…). |

No header at all means the cache wasn't involved: the answer came from the WAF, a challenge, the access gate or an edge redirect.

## Cache-Status

The standard header from RFC 9211, with the reason in `detail`:

```http
Cache-Status: OpenWAF; hit
Cache-Status: OpenWAF; fwd=uri-miss; stored
Cache-Status: OpenWAF; fwd=stale; fwd-status=304
Cache-Status: OpenWAF; fwd=bypass; detail=set-cookie
```

| `detail` | With | Meaning |
|---|---|---|
| `stored` | `MISS`, `EXPIRED` | The response was stored. |
| `not-stored`, `busy` | `MISS` | Storable, but this copy wasn't stored. |
| `set-cookie` | `BYPASS` | The response sets a cookie. |
| `private`, `no-store` | `BYPASS` | The origin's `Cache-Control` forbids storing it. |
| `auth` | `BYPASS` | The request had `Authorization` and the response didn't allow sharing it. |
| `vary` | `BYPASS`, `DYNAMIC` | An unsupported `Vary`, or the request carried a header of a rule's `vary_bypass_if_present`. |
| `size` | `BYPASS` | Larger than the maximum object size. |
| `deception` | `BYPASS` | Blocked by the cache deception armor. |
| `status` | `BYPASS` | The status code can't be cached. |
| `uncacheable` | `BYPASS` | No freshness information and no default TTL applies (for example HTML made eligible by a rule without a TTL). |
| `personal` | `BYPASS` | A personal document requested with cookies or `Authorization`, without explicit freshness. |
| `rule` | `BYPASS`, `DYNAMIC` | A rule's status TTL of `-1`, or a `bypass` rule. |
| `invalid`, `encoding` | `BYPASS` | An invalid `Cache-Control`, or a content encoding other than gzip or Brotli. |
| `extension` | `DYNAMIC` | The extension isn't in the default list and no rule makes the path eligible. |
| `disabled`, `dev-mode` | `DYNAMIC` | The cache is off, or development mode is on. |
| `method`, `upgrade`, `body` | `DYNAMIC` | Not a `GET`/`HEAD`, a WebSocket upgrade, or a request with a body. |
| `path`, `query`, `key` | `DYNAMIC` | An ambiguous path, a query with caching level *No query string*, or a key that is too long. |

## Age

Sent on `HIT`, `STALE`, `UPDATING` and `REVALIDATED`: the seconds since the object was stored or last revalidated. An `Age` sent by your origin on a response that wasn't cached is forwarded unchanged.

## Headers the edge removes

`OpenWAF-CDN-Cache-Control` and `Cache-Tag` are meant for the edge only and are removed before the response reaches the visitor. An `X-Cache-Status` sent by your origin is replaced.
