Cache TTL: How Long a Cached Copy Stays Fresh

Cache TTL (time to live) is how long a cache may reuse a stored copy. How to choose one for screenshots, with tested code and a decision table.

By Simon Chiu · Updated

A cache TTL (time to live) is the length of time a cache may reuse a stored copy without asking the source again; when the TTL runs out, the copy is stale and must be fetched or revalidated.

In HTTP the TTL is usually max-age in the Cache-Control header. In a screenshot service it is how long a render stays reusable and, often, how long its public link keeps working. This entry is part of the screenshot glossary.

What it means in plain terms

A TTL is a "best before" date. Before it, the cache serves its copy without a second thought. After it, the cache has to check. A short TTL means fresher images and more work. A long TTL means less work and a longer window in which a changed page still shows its old screenshot.

 rendered            TTL = 6 h                       expired
    │◀──────────── fresh: reuse, no work ────────────▶│◀── stale: re-render or revalidate
    0h          age 2h                                6h
                (Age header: 7200,
                 4 h of life left)

How it works technically

Every cache tracks two numbers: the TTL it was given and the age of the copy. Remaining life is TTL minus age. HTTP passes the age along in the Age header, so a copy that sat on a CDN for two hours arrives at the browser with two hours already used.

There can be two TTLs in one response. max-age is for browsers; s-maxage is for shared caches such as CDNs. We measured this on 2026-10-15 with the Playwright script below on https://www.wikipedia.org/:

Response Cache-Control Age (s) Browser life left (s) CDN life left (s)
HTML page s-maxage=86400, must-revalidate, max-age=3600 62,461 −58,861 23,939
Logo PNG s-maxage=86400, max-age=86400 62,982 23,418 23,418

The HTML was still fresh for the CDN but already stale for a browser, which must revalidate before using it. Ages and remaining times change on every run. https://example.com sent no Cache-Control at all, only an Age, so its TTL is up to each cache's heuristics.

Choosing a TTL for screenshots

Use Suggested TTL Why
OG image of a published post 7 to 30 days Content rarely changes; version the URL when it does
Product or pricing page preview 1 day Prices change, but not hourly
Link preview for user-submitted URLs 1 to 7 days Many repeats of popular links; staleness is minor
Dashboard or report image 1 hour or less Data changes; old numbers mislead
Uptime or visual monitoring Shortest possible, or no reuse The point is to see the page now
Private screenshot behind login Short, and purge after download Limits how long a public link exists

These are starting points from how the inputs change, not measured optimums. Pair long TTLs with cache invalidation by URL versioning.

How to set it

Puppeteer: a TTL for your own screenshot store

Headless browsers apply HTTP TTLs to the page's own assets. The TTL of the screenshots you produce is your code's job. This version uses the file's modification time. The first run printed missing, rendered; the second fresh (0 s old), expires in 21600 s:

import puppeteer from 'puppeteer';
import { stat } from 'node:fs/promises';

const TTL_SECONDS = 6 * 60 * 60; // 6 hours
const url = 'https://example.com/';
const file = 'ttl-example.png';

const info = await stat(file).catch(() => null);
const ageSeconds = info ? (Date.now() - info.mtimeMs) / 1000 : Infinity;

if (ageSeconds < TTL_SECONDS) {
  console.log(`fresh (${Math.round(ageSeconds)} s old), expires in ${Math.round(TTL_SECONDS - ageSeconds)} s`);
} else {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle0' });
    await page.screenshot({ path: file });
    console.log(info ? 'expired, rendered again' : 'missing, rendered');
  } finally {
    await browser.close();
  }
}

Playwright: read the TTLs a page declares

This produced the table above. Run it on your own site to see what browsers and CDNs are told:

import { chromium } from 'playwright';

function directive(cacheControl, name) {
  const m = new RegExp(`(?:^|,)\\s*${name}=(\\d+)`, 'i').exec(cacheControl);
  return m ? Number(m[1]) : null;
}

function ttlReport(headers) {
  const cc = headers['cache-control'] ?? '';
  const age = Number(headers['age'] ?? 0);
  const browserTtl = directive(cc, 'max-age');               // private caches (the browser)
  const sharedTtl = directive(cc, 's-maxage') ?? browserTtl; // CDNs and proxies
  return {
    cacheControl: cc || '-',
    age,
    browserLeft: browserTtl === null ? null : browserTtl - age,
    sharedLeft: sharedTtl === null ? null : sharedTtl - age,
  };
}

const browser = await chromium.launch();
try {
  const page = await browser.newPage();
  const rows = [];
  page.on('response', async (res) => {
    rows.push({ url: res.url().slice(0, 60), ...ttlReport(await res.allHeaders()) });
  });
  await page.goto(process.argv[2] ?? 'https://example.com/', { waitUntil: 'networkidle' });
  console.table(rows);
} finally {
  await browser.close();
}

Tested on 2026-10-15 with Node 22.18.0, [email protected] (run with headless: 'shell') and [email protected].

RenderScreenshot

Set the TTL in seconds with cache.ttl (POST) or cache_ttl (GET), from 3,600 (1 hour) to 2,592,000 (30 days); the default is 86,400 (24 hours). In the Node.js SDK it is .cacheTtl(seconds):

import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);
const image = await client.take(
  TakeOptions.url('https://example.com/blog/launch').preset('og_card').cacheTtl(7 * 24 * 60 * 60) // 7 days
);

The TTL does two jobs, as the caching reference explains. It sets X-Cache-Expires, how long the public X-Cache-URL copy is kept. And when you send cache.available, it is the maximum age of an earlier render this request will accept, decided by the TTL in the current request. This snippet was not run for this page.

Common mistakes

  • Minutes or milliseconds instead of seconds. ttl: 60 is below the API minimum and fails with invalid_cache_ttl; ttl: 86400000 is far above the maximum.
  • One TTL for everything. A monitoring capture and a blog card have opposite needs.
  • Forgetting downstream TTLs. A CDN or social platform may keep the image longer than your TTL.
  • Linking to a copy after it expires. Store the image in your own bucket if a link must outlive the TTL.
  • Reading max-age alone. Subtract Age, and check s-maxage for CDNs.

To pick a TTL per request and reuse renders inside it for 0 credits, get a free API key.

Frequently asked questions

What is a good cache TTL for screenshots?
It depends on how fast the page changes and how bad a stale image is. Social cards for published posts can live for days or weeks; monitoring captures should live for the shortest time the cache allows.
What happens when the TTL ends?
The copy becomes stale. A cache either deletes it or keeps it and checks with the origin before reuse. The next request then gets a fresh copy or a 304 Not Modified.
What TTL range does RenderScreenshot accept?
cache.ttl (cache_ttl on GET) takes 3600 to 2592000 seconds, that is 1 hour to 30 days. The default is 86400 seconds, 24 hours. Other values return invalid_cache_ttl.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.