# Cache rules

> Bypass the cache or make more requests eligible by path, extension, cookie or query parameter, with edge and browser TTLs per rule.

Cache rules change the [default behavior](https://developers.openwaf.dev/cache/default-behavior/) for the requests they match. Use them to keep admin pages and logged-in visitors out of the cache, or to cache HTML.

## How rules are evaluated

- A zone (and each name with its own list) has up to **25 rules**, evaluated in order for `GET` and `HEAD` requests.
- When several rules match, **the last matching rule wins for each setting**. Put broad `cache` rules first and the `bypass` rules that must always win after them.
- A name either inherits the zone's rules that apply to it or has its own list, which replaces the zone's for that name.

## Conditions

Every list you fill must match (AND); inside one list, any value may match (OR). A rule without conditions matches every request. Each list holds up to 32 values.

| Field | Dashboard | Matches when |
|---|---|---|
| `match.path_prefix` | Path starts with | The path starts with one of the values (each starts with `/`). |
| `match.path_exact` | Path is exactly | The path is exactly one of the values. |
| `match.extensions` | File extension | The last segment's extension is one of the values, without the dot; `""` = no extension (`/`, `/about`). |
| `match.cookie_present` | Request has cookie | The request carries one of these cookies; a trailing `*` matches a prefix (`wordpress_logged_in_*`). |
| `match.query_present` | URL has parameter | The URL has this query parameter, with or without a value; a trailing `*` matches a prefix. |
| `hosts` | Names | Zone rules only: the names the rule applies to; empty = every name. |

Because the lists of one rule are combined with AND, put exact paths and prefixes in separate rules when either should match.

## Actions

| `action` | Dashboard | Effect |
|---|---|---|
| `bypass` | Bypass cache | Never looked up or stored (`DYNAMIC`). Takes no parameters. |
| `cache` | Eligible for cache | Eligible even if the extension isn't in the default list (HTML, JSON…), with optional parameters below. |

### Parameters of a cache rule

| Parameter | Values | Effect |
|---|---|---|
| `edge_ttl.mode` | `respect_origin` | The origin's headers; without them, the [default TTLs](https://developers.openwaf.dev/cache/default-behavior/#default-ttls) (only on the default extensions). |
| | `bypass_by_default` | The origin's headers; without them, not stored. |
| | `override` (with `ttl_s`) | Ignore the origin's `Cache-Control` and keep the response `ttl_s` seconds. Removes `Set-Cookie` from the stored copy and disables the deception armor for these requests. |
| `edge_ttl.status` | `[{"from": 200, "to": 299, "ttl_s": 300}]` | TTL per status code range, in any mode: `-1` = don't store, `0` = store and always revalidate. Up to 10 ranges. |
| `browser_ttl` | `{"mode": "respect_origin"}` or `{"mode": "override", "ttl_s": N}` | `override` raises the browser `max-age` to at least `N`. |
| `query` | `{"mode": …, "sort": …, "params": […]}` | Query policy in the key for these requests. See [Cache keys](https://developers.openwaf.dev/cache/cache-keys/#query-string). |
| `serve_stale` | `{"while_revalidate": …, "on_error_max_s": …}` | Stale content for these requests. |
| `vary_bypass_if_present` | Lower-case request header names | See [below](#vary-bypass). |

TTLs go from 1 second to 1 year; `on_error_max_s` up to 30 days.

> **Caution**
>
> `override` stores responses even if they set cookies. Use it only for pages that are the same for every visitor, and add a `bypass` rule after it for logged-in visitors (their session cookies).


### Skip the cache on some request headers

`vary_bypass_if_present` (**Skip the cache if the request has these headers**) lists request headers, such as the `rsc`, `next-router-state-tree`, `next-router-prefetch` and `next-router-segment-prefetch` headers of Next.js client navigation. A request carrying any of them is neither looked up nor stored (`DYNAMIC`, `detail=vary`). Requests without them get the normal cached version, even if the origin lists those headers in `Vary`. `accept-encoding`, `accept` and `origin` can't be listed: they already vary the cache.

## Templates

The dashboard (**Caching › Cache rules › Start from a template**) fills the rule editor; nothing changes until you save.

| Template | Rules |
|---|---|
| WordPress | Bypass `/wp-admin/` and `/wp-login.php`, and visitors with WordPress or WooCommerce session cookies. Optionally also caches anonymous HTML for 5 minutes. |
| Next.js | Caches pages (paths without extension) for up to 10 minutes, only `200` answers, ignoring ad parameters in the key. Never caches `/api/`, login, account, cart or checkout paths, visitors with a session cookie or Next.js navigation requests (RSC headers, `?_rsc`). |
| Static site | HTML for 5 minutes, served stale while it refreshes or if the origin fails. |
| Defaults only | Removes every rule. |
| Cache everything | Every response eligible, following the origin's `Cache-Control`. |
| Bypass cache for everything | Nothing is cached. |

> **Tip**
>
> With the Next.js template, adjust the session cookie names to your site, and purge the HTML after every deploy so pages don't point at the previous build's chunks. A [purge-only token](https://developers.openwaf.dev/cache/purge/#ci) does that from CI.


## Add rules

**Dashboard**

In **Caching › Cache rules**, select **Add rule**, fill **When incoming requests match…** and **Then…**, and **Apply**. Reorder with the arrows, then **Save changes**.

**CLI**

```sh
openwaf-cloud ctl zones set example.com 'cache.rules:=[
  {"match": {"extensions": ["html", ""]}, "action": "cache",
   "edge_ttl": {"mode": "override", "ttl_s": 300}},
  {"match": {"path_prefix": ["/wp-admin/", "/wp-login.php"]}, "action": "bypass"},
  {"match": {"cookie_present": ["wordpress_logged_in_*", "wp-postpass_*"]}, "action": "bypass"}
]' --reason "WordPress cache rules"
```

**API**

```sh
curl -s -X PATCH https://dash.openwaf.dev/v1/orgs/$ORG/zones/$ZONE/edge \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Change-Reason: bypass admin" \
  -d '{"cache": {"rules": [{"match": {"path_prefix": ["/wp-admin/"]}, "action": "bypass"}]}}'
```

**MCP**

`update_settings` with `{"zone": "example.com", "settings": {"cache": {"rules": [{"match": {"path_prefix": ["/wp-admin/"]}, "action": "bypass"}]}}, "reason": "bypass admin"}`.


`rules` is always replaced as a whole: send the complete list; `[]` removes every rule. Rules without an `id` get `r1`, `r2`…

## Check a rule

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

For HTML, ask twice: the second answer should be `HIT`. A page that stays `MISS` or `BYPASS` isn't being stored; `Cache-Status` says why in `detail`. See [Troubleshooting](https://developers.openwaf.dev/cache/troubleshooting/).
