Skip to content

Type an option name like full_page, an error code, or a topic.

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.

Request
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:

First response
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:

Second response
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. 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).

Request: keep the copy for one day
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 optionWhy
access_key, signature, expiresThey say who is asking and for how long, not what to draw.
cache, cache_ttlThey control the cache itself.
async, webhook_url, webhook_signThey change how the result is delivered.
response_typeThe file is the same whether you get it directly or as JSON.
timeoutIt 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.

#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 the same fact is in the body, in the cached field:

Request
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&cache=true&response_type=json"
Response, status 200
{
  "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.

Request: a new label gives a new render
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.

#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.

HeaderValueMeaning
Cache-Controlpublic, max-age=14399Anyone 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 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:

Request
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"
Response, status 304
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.

#Where to go next