# Troubleshooting the cache

> Find out why a response isn't cached, why visitors still see old content after a change, and why Early Hints don't appear.

Start with the response headers of the URL:

```sh
curl -sI https://www.example.com/path | grep -i -E 'x-cache-status|cache-status|age|cache-control|set-cookie|vary'
```

`Cache-Status` says why in `detail`; every value is in [Cache status headers](https://developers.openwaf.dev/cache/cache-status/#cache-status).

## No X-Cache-Status header

The cache wasn't involved: the answer came from the edge itself (a WAF block, a challenge, the access gate or a redirect), or the name isn't proxied through OpenWAF. Check that the request reaches the edge and, after a settings change, that the new version has propagated (**Changes › Propagation**).

## Always DYNAMIC

| `detail` | Fix |
|---|---|
| `extension` | The file type isn't in the [default list](https://developers.openwaf.dev/cache/default-behavior/#default-extensions). Add a `cache` [rule](https://developers.openwaf.dev/cache/cache-rules/) for it (HTML, JSON…). |
| `rule` | A `bypass` rule matches. Remember the last matching rule wins. |
| `dev-mode` | [Development mode](https://developers.openwaf.dev/cache/configure/#development-mode) is on for the zone or the name. |
| `disabled` | The cache is off for this name: check **Scope**, a name can have its own value. |
| `query` | Caching level is *No query string* and the URL has a query. |
| `vary` | The request carries a header listed in a rule's `vary_bypass_if_present`. |
| `method`, `upgrade`, `body` | Only `GET` and `HEAD` without a body are cached. |
| `path` | The path contains `;`, an encoded `/` or `\`, or ends a segment with a dot. |

## Always MISS or BYPASS

| `detail` | Fix |
|---|---|
| `set-cookie` | The origin sets a cookie on this response. Stop setting it on public assets, send `Cache-Control: private="Set-Cookie"`, or use a rule with `edge_ttl.mode` `override` if the page is the same for everyone. |
| `private`, `no-store` | The origin's `Cache-Control` forbids shared caches. Send `public, max-age=…`, or target the edge with `OpenWAF-CDN-Cache-Control`. |
| `uncacheable` | No `Cache-Control`, `Expires` or default TTL applies, typically HTML made eligible by a rule. Have the origin send freshness or set an edge TTL in the rule. |
| `vary` | The origin sends `Vary` with a header other than `Accept-Encoding`, `Accept` (images) or `Origin`. Remove it, or list it in a rule's `vary_bypass_if_present`. |
| `auth` | The request had `Authorization`; the response must say `public`, `s-maxage` or `must-revalidate` to be shared. |
| `size` | Larger than `cache.max_object_mb`. Raise it (up to 512). |
| `deception` | The content type doesn't match the extension. Fix the origin's `Content-Type`. |
| `personal` | A document (`pdf`, `docx`…) requested with cookies, without explicit freshness from the origin. |
| `status` | The status code can't be stored. |

A `MISS` on the second request can also mean it reached another edge node: each node has its own cache.

## Visitors still see old content

- [Purge](https://developers.openwaf.dev/cache/purge/) the URLs, a prefix, the hostname or a tag. Check **Recent purges** shows it applied on every node.
- With a URL purge, the query string counts: `app.css?v=2` and `app.css` are different URLs.
- Browsers may keep their own copy for the `max-age` they received. A purge can't clear browser caches; use versioned asset names or shorter `max-age` values.
- After a deploy of a site with hashed assets, purge the HTML: old pages point to the previous build's files.

## Purge answers 429

The organization hit the [purge limits](https://developers.openwaf.dev/cache/purge/#limits). Wait for the `Retry-After` seconds, or group values: one purge can carry up to 100 values, and a prefix or tag often replaces many URLs.

## No 103 Early Hints

- The page was served from the cache (`HIT`): hints are only sent when the request waits for the origin.
- The node hasn't seen a shareable `200` HTML answer for the page yet; request it again.
- The request isn't a navigation: it needs `Accept: text/html`, `GET`, no `Range`, HTTP/1.1 or HTTP/2.
- The links point to an origin not listed in `early_hints.extra_origins`, or aren't `https`.

See [Early Hints](https://developers.openwaf.dev/cache/early-hints/).
