# Vary Header: Cache Keys, Vary: Origin and Accept-Encoding

> Vary lists the request headers a response depends on, so caches keep separate copies. Vary: Accept-Encoding, Vary: Origin for CORS, Vary: *, and CDN cache keys.

Source: https://howhttpworks.com/headers/vary
Last reviewed: 2026-10-04

> **TL;DR:** `Vary` tells caches which request headers changed the response, so one URL can have several cached variants. Set `Vary: Accept-Encoding` when you compress and `Vary: Origin` whenever `Access-Control-Allow-Origin` depends on the request.

## How a cache key really works

A cache keys on the method and URL, plus every request header named in the stored response's `Vary` (RFC 9111 section 4.1). Two requests that differ only in an unlisted header share an entry.

```http
GET /app HTTP/1.1
Accept-Encoding: br

HTTP/1.1 200 OK
Content-Encoding: br
Vary: Accept-Encoding
```

A later request with `Accept-Encoding: gzip` does not match, so the cache goes to the origin and stores a second variant. Matching is on the exact header values, so CDNs usually normalise `Accept-Encoding` first to avoid needless variants.

## Headers you will actually vary on

- **`Accept-Encoding`**. Compression. Servers add it automatically in nginx (`gzip_vary on;`) and Express `compression` middleware.
- **`Origin`**. Dynamic CORS. See below.
- **`Accept`**. API content negotiation (`application/json` vs `text/html`), and image formats such as `image/avif`.
- **`Accept-Language`**. Language redirects without separate URLs; better to use `/en/` paths.
- **`Authorization` or `Cookie`**. Personalised responses. Better to mark them `Cache-Control: private` than to Vary; varying on `Cookie` fragments a shared cache into one entry per user.
- **`User-Agent`**. Almost always a mistake. Use Client Hints or responsive CSS instead.

## Vary: Origin and CORS cache poisoning

If your server echoes the caller's origin:

```http
Access-Control-Allow-Origin: https://app.example.com
```

then add:

```http
Vary: Origin
```

Without it a CDN or the browser cache can store the response for `https://app.example.com` and serve it to `https://admin.example.com`, whose browser then logs:

```text
The 'Access-Control-Allow-Origin' header has a value 'https://app.example.com' that is not equal to the supplied origin.
```

It often looks intermittent and disappears with a hard refresh. Also vary on origin even when you do not send an ACAO header for disallowed origins, otherwise a no-CORS response can be cached and reused for a CORS request. See [Access-Control-Allow-Origin](https://howhttpworks.com/headers/access-control-allow-origin).

## Vary: *

`Vary: *` says the response depends on factors outside request headers, such as client IP. Under RFC 9111 a cache never treats a stored response as a match without validation. It is blunt; `Cache-Control: no-store` or `private` states your intent better.

## Configuration

nginx:

```nginx
gzip on;
gzip_vary on;   # adds Vary: Accept-Encoding for compressed responses

location /api/ {
    add_header Vary Origin always;
}
```

nginx `add_header Vary ...` appends another `Vary` line rather than merging; that is allowed, as caches combine them. Check the combined result with curl.

Express:

```javascript
res.vary('Origin')          // appends instead of overwriting
res.vary('Accept-Encoding')
```

Cloudflare: Vary support is limited (see the FAQ). To cache by origin, Accept or other headers, use a custom cache key via Cache Rules (Enterprise) or key on a URL parameter. CloudFront: forward the headers in the cache policy; they become part of the cache key. A `Vary` header that names a header CloudFront does not forward is a common cause of wrong content.

## Verify

```bash
curl -sI https://example.com/app -H 'Accept-Encoding: br' | grep -iE '^(vary|content-encoding|age|x-cache|cf-cache-status)'
curl -sI https://api.example.com/data -H 'Origin: https://app.example.com' | grep -i '^vary'
```

## Mistakes

- Missing `Vary: Accept` when one URL serves JSON or HTML by negotiation, leaving the CDN to return whichever it cached first.
- `Vary: Cookie` on public pages that merely read a tracking cookie; the hit rate drops to near zero.
- Setting `Vary` in the app but having a proxy overwrite it. Inspect at the client, not just the origin.
- Using `Vary: User-Agent` for mobile detection. Prefer one responsive page.

## Related

- [Cache-Control](https://howhttpworks.com/headers/cache-control), [Accept-Encoding](https://howhttpworks.com/headers/accept-encoding), [Accept-Language](https://howhttpworks.com/headers/accept-language), [Origin](https://howhttpworks.com/headers/origin), [ETag](https://howhttpworks.com/headers/etag)
- [CORS guide](https://howhttpworks.com/guides/cors) and [HTTP caching guide](https://howhttpworks.com/guides/headers-and-caching)
