# Caching

Source: https://capacms.com/docs/concepts/caching

How published reads stay in the CDN cache, what a publish purges, and what your own site may still hold.

Every published read goes through Capa's CDN at `https://cdn.capacms.com`. The CDN keeps the response, and Capa purges it when the content behind it changes.

You do not configure this. You only choose which key you read with.

## Which reads are cached

The key's environment decides. A production key reads published content, so its responses can be shared. A development key reads drafts, which have no business in a shared cache.

| Response                                 | Production key                       | Development key   |
| ---------------------------------------- | ------------------------------------ | ----------------- |
| `GET /api/entries/...`, 200              | Cached                               | Never cached      |
| `GET /api/graphql`, no errors            | Cached                               | Never cached      |
| `POST /api/graphql`                      | Never cached                         | Never cached      |
| `GET /v2/api/...` and `/v3/api/...`, 200 | Cached                               | Never cached      |
| `GET /v2/seo/...`, 200                   | Cached                               | Never cached      |
| `GET /files/...`, which takes no key     | Cached for a year                    | Cached for a year |
| Any error                                | Not cached, with one exception below | Not cached        |

The exception: a production key's `404 entry_not_found` for a well-formed id is cached like a 200. When an entry is taken down, the edge serves the 404 in its place, and the publish that brings it back purges the 404.

A GraphQL read that selects `me`, `__schema` or `__type` is never cached.

## The headers

A production key's 200 on `/api/` carries:

| Header              | Value                                 |
| ------------------- | ------------------------------------- |
| `Cache-Control`     | `public, max-age=60`                  |
| `Surrogate-Control` | the edge lifetime, with stale windows |
| `Surrogate-Key`     | the keys a purge targets (below)      |
| `ETag`              | a strong tag over the body            |

A development key's response carries `Cache-Control: no-store, no-cache` and no `ETag`.

`Cache-Control: max-age=60` is for your side: a browser or your own server may reuse the response for 60 seconds. The edge keeps it longer and relies on purges instead.

## What a publish purges

Each cached response is tagged with surrogate keys. A change purges the keys it touches, so only the affected responses are dropped.

| Key                        | One per                                                                                   |
| -------------------------- | ----------------------------------------------------------------------------------------- |
| `t:<projectId>`            | response                                                                                  |
| `c:<projectId>:<contract>` | response                                                                                  |
| `k:<keyId>`                | response                                                                                  |
| `m:<modelId>`              | the root model, every expanded relation's model, and every model a filter or sort reaches |
| `e:<entryId>`              | entry in the response, and every expanded entry                                           |
| `f:<fileId>`               | file a media value renders                                                                |

| Change                                     | What it purges                                                                                  |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| Publish an entry                           | its `e:` key and its model's `m:` key, so every response showing it and every list of its model |
| Move an entry to another folder            | the same keys                                                                                   |
| Unpublish or delete an entry               | the same keys, hard                                                                             |
| Delete a model                             | its `m:` key, hard                                                                              |
| Delete a project                           | its `t:` key, hard                                                                              |
| Edit a file's alt text                     | its `f:` key                                                                                    |
| Delete a file                              | its `f:` key, hard                                                                              |
| Deactivate, rotate, narrow or expire a key | its `k:` key                                                                                    |

A publish marks the old copies stale. The edge can serve the stale copy once more while it fetches the new one, so a read in the same moment as the publish may still see the old body. A hard purge drops the old copy at once: unpublish and delete use it so taken-down content never lingers.

A response with too many entries to list keeps its `t:`, `c:`, `k:`, `m:` and `f:` keys and drops the `e:` keys. It then carries `Capa-Cache-Scope: model`. A purge is broader than it needed to be, never narrower.

## Revalidate with an ETag

Send the `ETag` back as `If-None-Match`. An unchanged response is a `304` with an empty body.

```bash
curl -sD - -o /dev/null 'https://cdn.capacms.com/api/entries/articles?limit=2' \
  -H "x-api-key: $CAPA_KEY" -H 'If-None-Match: "3f9c1a…"'
```

The tag covers `data` and `page`, never `meta`, because `meta.requestId` changes on every request.

## Rate limits

Capa does not limit how many requests a key makes per minute. It limits how many reads run at once, so a build that sends many reads in parallel can get [`429 rate_limit_exceeded`](https://capacms.com/docs/errors/rate_limit_exceeded) with a `Retry-After` in seconds. Send fewer at a time, and retry after that many seconds.

## The legacy API

`/v2/api` and `/v3/api` cache the same way for a `pk_` key: `public, max-age=60` for your side, a longer edge lifetime, and a purge of every cached copy of the model when one of its entries is published. An `sk_` key is never cached.

After a publish, allow for the 60 seconds your side may still hold.

## Files and images

A file under `/files/` is cached for a year, at the edge and in the browser. Every resized or converted variant carries the file's own key, so deleting the file purges every variant at once. Replacing a file does not purge the edge, so the old copy can stay for up to a year. Upload a new file when a change must show. See [Images](https://capacms.com/docs/guides/images).

## Your own caches

The CDN is Capa's half. Anything your site keeps on top is yours to expire:

* **Your server or framework.** Next.js, for one, can keep fetch results in its data cache. See [Next.js](https://capacms.com/docs/guides/nextjs) for tags and webhook revalidation.
* **A CDN of your own.** The SDK gives you the response's surrogate keys as `cacheTags`, so you can purge by the same keys.
* **The browser.** `max-age=60` lets it reuse a response for a minute.

## Related

* [Drafts and publishing](https://capacms.com/docs/concepts/drafts-and-publishing) for what each key sees.
* [Entries reference: caching](https://capacms.com/docs/api/entries#caching) for every header.
* [Webhooks](https://capacms.com/docs/webhooks) to hear about a publish the moment it happens.
