# Caching

Store a screenshot once and reuse it for free. How cache, cache_ttl and cache_key work, and how to force a fresh render.

Add `cache=true` and run the same request twice. The `-D -` part tells curl to print the response headers.

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&cache=true" \
  --output example.png -D -
```

The first time, the page is rendered and the result is stored. These are the headers that matter here:

```http
HTTP/1.1 200 OK
cache-control: public, max-age=14399
content-type: image/png
etag: "6b8cc079867c41a8bf3f40aec1e5477d"
x-cache: MISS
x-quota-remaining: 4997
x-render-ms: 167
```

The second time, you get the stored copy:

```http
HTTP/1.1 200 OK
cache-control: public, max-age=14380
content-type: image/png
etag: "6b8cc079867c41a8bf3f40aec1e5477d"
x-cache: HIT
x-quota-remaining: 4997
x-render-ms: 167
```

`x-cache` went from `MISS` to `HIT`, and `x-quota-remaining` did not go down. A cache hit is free: it does not use a screenshot from your [quota](https://curlshot.com/docs/usage-and-limits.md). It is also faster, because nothing is rendered.

On a hit, `x-render-ms` is the time the original render took, not the time of this answer.

## What caching does

A cache is a shelf of finished results. When the same request comes in again, we hand back the copy from the shelf and skip the render.

Caching is off unless you ask for it. Without `cache=true`, every request opens the page again and counts as a new screenshot.

Turn it on when the same screenshot is requested many times. A thumbnail in a list is a good example. Leave it off when you always need the page as it looks right now.

## The three options

### cache

Serve a stored copy when the same request was made before. If there is no stored copy yet, the page is rendered and the result is stored for next time.

The default is `false`.

### cache_ttl

How long a stored copy stays valid, in seconds. TTL is short for "time to live". After that time, the next request renders the page again and stores the new result.

The default is `14400`. That is 4 hours.

The smallest value is `60` (one minute). The largest is `2592000` (30 days).

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY\
&url=https://en.wikipedia.org/wiki/Eiffel_Tower\
&cache=true\
&cache_ttl=86400" \
  --output eiffel.png
```

### cache_key

A short label of your choice that is stored together with the copy. Change the label and the old copy no longer matches, so you get a fresh render.

There is no default. Leave it out and it is not applied.

It can be 1 to 64 characters: letters, digits, dots, dashes and underscores.

## What counts as the same request

Two requests are the same when every option that changes the screenshot is the same. That means the `url`, the size, the format, the blocking options, the injected CSS, and so on.

Options are compared after the defaults are filled in. So `format=png` and no `format` at all are the same request, because `png` is the default.

These options do not change the screenshot, so they are ignored when we look for a stored copy:

| Ignored option | Why |
| --- | --- |
| `access_key`, `signature`, `expires` | They say who is asking and for how long, not what to draw. |
| `cache`, `cache_ttl` | They control the cache itself. |
| `async`, `webhook_url`, `webhook_sign` | They change how the result is delivered. |
| `response_type` | The file is the same whether you get it directly or as JSON. |
| `timeout` | It only changes how long we wait. |

`cache_key` is the one exception among the cache options. It is part of the match on purpose, so you can use it to force a refresh.

Stored copies belong to your account. Another customer who captures the same page never gets your copy, and you never get theirs.

> **Two keys, one shelf**
>
> If you have several API keys on one account, they share the same stored copies. A request made with one key can be a cache hit for another.

## How to tell a hit from a miss

Every screenshot answer carries the `X-Cache` header. It says `HIT` when you got a stored copy and `MISS` when the page was rendered.

With [`response_type=json`](https://curlshot.com/docs/options.md#response_type) the same fact is in the body, in the `cached` field:

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&cache=true&response_type=json"
```

```json
{
  "id": "6b8cc079867c41a8bf3f40aec1e5477d",
  "url": "https://curlshot.com/api/v1/files/6b8cc079867c41a8bf3f40aec1e5477d.png?expires=1791083483&token=wr0Y6nTqXC-yLpWGbch_RVMyZqjGaUgnu2Eeq-shclg",
  "format": "png",
  "width": 1280,
  "height": 1024,
  "bytes": 22195,
  "render_ms": 167,
  "cached": true,
  "expires_at": "2026-10-04T03:11:23.563Z"
}
```

`expires_at` is the moment the stored copy runs out.

The `url` is a ready-made link to the stored file. It needs no access key: the `token` inside it is the permission. It stops working at `expires_at`, so do not save it as a permanent address.

## Force a fresh render

Sometimes the page changed and you do not want to wait for the stored copy to run out. You have two choices.

**Change `cache_key`.** Any new value works. A version number or the date the page was last edited are common picks.

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY\
&url=https://news.ycombinator.com\
&cache=true\
&cache_key=2026-01-15" \
  --output hn.png
```

The first request with a new `cache_key` is a `MISS` and uses one screenshot. Requests after it, with the same label, are hits again.

**Leave `cache` out for one request.** A request without `cache=true` always renders the page.

> **Tip**
>
> If your own data has an "updated at" time, use it as the `cache_key`. The screenshot is then refreshed exactly when your content changes, and never in between.

## Caching in browsers and CDNs

An answer made with `cache=true` carries two standard headers. They tell a browser or a CDN (a network of servers that keeps copies of files close to your visitors) how long it may keep the file.

| Header | Value | Meaning |
| --- | --- | --- |
| `Cache-Control` | `public, max-age=14399` | Anyone may keep this file for that many seconds. The number is the time the stored copy has left. |
| `ETag` | `"6b8cc079867c41a8bf3f40aec1e5477d"` | A name for this exact version of the file. |

So when you put a screenshot link in an `<img>` tag, a visitor's browser keeps the screenshot too, and does not ask us again on every page view. Use a [signed link](https://curlshot.com/docs/signed-links.md) for that, so your access key cannot be reused.

When the browser's copy runs out, it asks again and sends the `ETag` back in an `If-None-Match` header. If the screenshot has not changed, we answer `304 Not Modified` with no body, and the browser reuses what it has. Browsers and CDNs do this on their own. Here it is by hand:

```bash
URL="https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&cache=true"

# Read the ETag of the stored copy from the headers.
ETAG=$(curl -s -o /dev/null -D - "$URL" | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r')

curl -o /dev/null -D - "$URL" -H "If-None-Match: $ETAG"
```

```http
HTTP/1.1 304 Not Modified
cache-control: public, max-age=14399
etag: "6b8cc079867c41a8bf3f40aec1e5477d"
x-cache: HIT
```

Without `cache=true` the answer says `Cache-Control: private, no-store` and has no `ETag`. Nobody keeps a copy, and every request is a new render.

> **Common mistakes**
>
> - **You expect caching without asking for it.** The default is `cache=false`. Every request without `cache=true` is rendered and counted.
> - **You change `cache_ttl` to refresh the screenshot.** `cache_ttl` is ignored when we look for a stored copy. Change `cache_key` instead.
> - **A value changes on every request.** A timestamp inside the `url`, or a random `cache_key`, makes every request different. You then never get a hit.
> - **The value is out of range.** A `cache_ttl` under `60` or over `2592000` returns [`invalid_options`](https://curlshot.com/docs/errors.md#invalid_options).

## Where to go next

- [Signed links](https://curlshot.com/docs/signed-links.md): put cached screenshots in a web page without exposing your key.
- [Usage and limits](https://curlshot.com/docs/usage-and-limits.md): what counts against your quota and what does not.
- [How to add website previews to your app](https://curlshot.com/docs/guides/website-previews.md): caching used in a real feature.
- [Options reference](https://curlshot.com/docs/options.md#cache): every option on one page.
