Proxy

# Response caching

An RFC 9111 shared cache in front of your app — policy per service, storage per proxy.

## The model

dash-proxy can run an **RFC 9111 shared cache** in front of your app — the standard HTTP caching model, driven entirely by the response headers your app already sets. Nothing is stored unless the app marks a response `public` with an `s-maxage` or `max-age`, and a response carrying `Set-Cookie` is refused unless you say otherwise — a shared cache replaying one client's cookie to the next is the worst thing it could do.

The configuration splits in two, on the policy/mechanism line:

- `proxy: cache:` — the **policy**, per service: what this app allows to be stored.
- `proxy: run: cache:` — the **storage**, proxy-wide: where entries live for every service on the proxy.

With a [load balancer](https://dash.zoolutions.llc/docs/load-balancing), the cache runs there — the one layer that sees the whole fleet.

## The policy (per service)

Only `enabled` is required; everything else keeps dash-proxy's own default until you set it:

```yaml
proxy:
  cache:
    enabled: true
    max_ttl: 300             # cap the lifetime the app asks for (seconds)
    max_body: 1_048_576      # largest response body to store (bytes)
    max_variants: 8          # representations one URL may hold under Vary
    vary_headers:            # request headers added to EVERY cache key
      - Accept-Encoding
    vary_cookies:            # cookie names added to EVERY cache key
      - locale
    allow_set_cookie: false  # refuse Set-Cookie responses (the safe default)
```

`max_ttl` means one mistaken `Cache-Control` cannot pin content until the next deploy. Responses bigger than `max_body` still reach the client — they are just not kept. Headers the app already names in `Vary` are keyed automatically per URL; `vary_headers` moves a header into the key for **every** path in the service, which is usually not what you want.

## The storage (proxy-wide)

```yaml
proxy:
  run:
    cache:
      store: redis://cache.example.com:6379/0   # or `memory` (the default)
      store_timeout: 2
      memory_size: 134_217_728
```

`memory` is a per-node cache. A `redis://` / `rediss://` URL is shared by every proxy pointed at it, so one fetch warms the whole fleet. The URL is delivered as `CACHE_STORE` in a `0600` env file on the host — never on the command line — so a password in it stays out of process listings and the audit log. Changing only the URL's *value* does not move the drift digest: run `dash proxy reboot` after rotating it.

`store_timeout` is the fail-open guard for a shared store: how long the store may take to answer before the request goes to the app instead. A slow or down store costs a cache hit, never a failed request.

## Administering it

```shell
dash proxy cache stats               # what the cache is holding
dash proxy cache stats --count       # entries and bytes per service
dash proxy cache stats --json        # the raw report
dash proxy cache purge               # drop this app's cached responses
dash proxy cache purge --path-prefix /assets
```

Both run on the layer that owns the cache — the load balancer when load balancing, else each proxy host.

## When it is not caching

A cache that quietly stores nothing is the usual first surprise. Start with `dash proxy cache stats`; dash-proxy also explains every refusal — check `dash proxy logs` for the reason, and the `cache_refusals_total` Prometheus metric (by `reason`) if you run with `metrics_port`. The common reasons:

- no `Cache-Control: public, max-age=...` on the app's response,
- a `Set-Cookie` header (with `allow_set_cookie: false`),
- a body over `max_body`,
- `variant_limit` from `max_variants`.