# Caching

Ask for the same page twice and the second answer is the rendering we already made for you. It arrives faster and costs no credit. This page explains when that happens, how to tell that it did, and how to turn it off.

> Caching is on unless you turn it off. If you are watching a page for changes, send `cache=0` so every request renders the page as it is now.

## How it works

A rendered file is kept under the exact request that produced it: the url, and every option you sent with it. Change any of them, a viewport, an image format, a selector, and you have described a different picture, so the page is rendered again. A screenshot and a PDF of one page are two entries for the same reason.

Entries belong to your own account. Nobody else's request is ever answered with a rendering you paid for, and yours is never answered with theirs, so what you render stays between you and us.

Identical requests that arrive at the same time are rendered once rather than once each. If you fan a batch of workers out over the same page, the first one renders it and the others are handed the result.

The cache is also how an [asynchronous batch](https://screenshotbuddy.io/documentation/batch-renders.md) hands its results over. There is no response for a queued render to travel back in, so the rendering is kept here and the batch gives you a link that fetches it. That is why an item of a batch may not turn caching off, and why its `cacheTtl` has a floor of its own.

## What it costs

A cached answer costs no credit. We render nothing, so there is nothing to charge for, and the answer arrives without waiting for a browser.

It is counted against a [rate limit](https://screenshotbuddy.io/documentation/rate-limits.md) of its own rather than against the one your renders come out of: 300 cached answers per minute, the same for every account whatever plan it is on. That is far more room than the render limit gives you, which is the point. A page that fetches twenty thumbnails the moment it loads is twenty hits, and it should not be able to spend a minute of the renders you were saving for real work.

Because a hit costs us nothing, it is served even when your credits have run out or your plan has lapsed. A request that has to render is refused in that situation; one we can answer from a rendering you already paid for is not.

## Reading the headers

Every rendered answer says where it came from.

| Header | Type | Description |
| --- | --- | --- |
| `X-Cache` | string | `HIT` means the file came out of the cache, so nothing was rendered and no credit was spent, and the `X-RateLimit` headers beside it report the cached budget of 300 rather than your render limit. `MISS` means the page was rendered for this request and the result was kept for the next one. The header is absent altogether when you sent `cache=0`: that request declined the cache rather than missing it. |
| `ETag` | string | A quoted fingerprint of the file itself, so two renderings that produced the same bytes carry the same tag. Keep it alongside whatever you did with the file and send it back on your next request to find out, for free, whether anything changed. |

## Asking whether anything changed

Send the `ETag` of the copy you already hold back as an `If-None-Match` header. If the entry is still the one that tag describes, the answer is `304` with no body at all: no credit, no file to download a second time, and nothing counted against either rate limit. It is the one answer the API gives away entirely, so a client that polls for changes can do so as often as it likes.

A conditional request the cache cannot answer, because you sent `cache=0` or because the entry has lapsed, renders the page first and then compares. If the fresh bytes carry the tag you hold, that is a `304` with no body too. This one costs what the render costs, a credit and a slot of your render budget, because the page had to be loaded to find out. What you save is the download, and the answer carries the `X-RateLimit` pair to say which budget paid for it.

Anything else is answered normally, with the file.

```bash
curl "https://api.screenshotbuddy.io/v1/snap?url=https%3A%2F%2Fexample.com" \
  -H "Authorization: Bearer {token}" \
  -H 'If-None-Match: "<etag>"' \
  --output screenshot.png
```

## Watching a page for changes

Put the two together and you have a monitor. Send `cache=0` with the `If-None-Match` of the last capture you kept, as often as the page deserves. A `304` means the page still looks exactly as it did. A `200` means it does not, and the body is the new capture, carrying the new `ETag` to hold for the next round.

Every round really loads the page, so what you are told is about the page rather than about our cache, and every round costs one render. That is the price of finding out.

Branch on the status rather than on the file you were writing to. A `304` has no body, so a downloader pointed straight at the copy you are keeping would empty it; write to a scratch name and keep it only when the status was `200`.

```bash
curl "https://api.screenshotbuddy.io/v1/snap?url=https%3A%2F%2Fexample.com&cache=0" \
  -H "Authorization: Bearer {token}" \
  -H 'If-None-Match: "<etag>"' \
  --write-out "%{http_code}\n" \
  --output capture-new.png
```

## How long a rendering is kept

A rendering is kept for 86400 seconds unless you say otherwise. Send `cacheTtl`, in seconds, to choose your own: at least `60` and at most `2592000`, which is thirty days. Pick it from how often the page changes rather than from how often you ask: a marketing page can sit at the maximum, a dashboard should not.

Once the time is up the next request renders the page again, and costs a credit again.

## Always rendering afresh

Send `cache=0` and the page is rendered as it is right now. That request neither reads the cache nor fills it, it carries no `X-Cache` header, and it costs a credit like any other render. Sending `cacheTtl` alongside it is rejected with a `422`: there is no lifetime to set on a rendering that is not being kept.
