CDN Edge Cache: Serving Images Close to Users

A CDN edge cache keeps copies of responses near users. How hits, misses and the Age header work, how to check them, and how screenshots are served.

By Simon Chiu · Updated

A CDN edge cache is a shared cache in a content delivery network's data centres (points of presence) that stores copies of responses close to users, so repeat requests are answered at the edge instead of travelling to the origin server.

For images such as screenshots and social cards, the edge cache decides how fast they load around the world and how long an old version keeps appearing after you change it. This entry is part of the screenshot glossary.

What it means in plain terms

Your origin server lives in one place. Your visitors do not. A CDN puts many small caches between them. The first visitor near Toronto makes the Toronto edge fetch the image from the origin (a miss). Every later visitor near Toronto gets the stored copy (a hit) until it expires. A visitor near Frankfurt starts the same process at the Frankfurt edge.

 visitor (Toronto) ──▶ edge YYZ ──miss──▶ origin (server or storage bucket)
                         │  ◀── image + Cache-Control
                         │  stores copy, Age starts at 0
 next visitor (Toronto) ─┘  hit: served from YYZ, Age = seconds since stored

 visitor (Frankfurt) ──▶ edge FRA ──miss──▶ origin    (each edge fills separately)

How it works technically

An edge cache works like any HTTP cache, with a few specific behaviours:

  • What it stores depends on the origin's Cache-Control header: s-maxage if present, else max-age, with private and no-store responses skipped. Many CDNs also cache only certain file types by default, such as images, and treat HTML as dynamic.
  • The key is normally the URL, sometimes plus selected headers. Two URLs that differ only in a query parameter are two entries.
  • The Age header tells you how long the copy has been at the edge. Browsers subtract it from their own TTL.
  • Status headers report hit or miss. Names vary by vendor: cf-cache-status (Cloudflare), x-cache (CloudFront, Fastly and others) and the standard Cache-Status header from RFC 9211.
  • Purges are per CDN. Deleting the file at the origin does not remove the edge copies; you purge through the CDN's API or wait for the TTL.

How to inspect it

On 2026-10-15, https://example.com and its script both came back with server: cloudflare, cf-cache-status: HIT and an Age above 7,000 seconds, even though neither sent a Cache-Control header.

Puppeteer: which page resources came from an edge

import puppeteer from 'puppeteer';

const EDGE_HEADERS = ['cache-status', 'cf-cache-status', 'x-cache', 'x-vercel-cache', 'age', 'server'];
const url = process.argv[2] ?? 'https://example.com/';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const rows = [];
  page.on('response', (res) => {
    const h = res.headers();
    const row = { url: res.url().slice(0, 60), fromBrowserCache: res.fromCache() };
    for (const name of EDGE_HEADERS) if (h[name]) row[name] = h[name];
    rows.push(row);
  });
  await page.goto(url, { waitUntil: 'networkidle0' });
  console.table(rows);
} finally {
  await browser.close();
}

fromBrowserCache separates the browser's own cache from the edge. In a fresh browser it is false for everything, so any HIT you see came from the CDN.

Playwright: fetch twice and compare

Playwright's API client makes plain HTTP requests without rendering a page. Both requests printed { status: 'HIT', age: '8156', pop: 'YYZ' }. Note that the cache-control: no-cache request header did not force a miss; the edge ignored it:

import { request } from 'playwright';

const url = process.argv[2] ?? 'https://example.com/';
const api = await request.newContext();
try {
  for (const attempt of [1, 2]) {
    const res = await api.get(url, { headers: { 'cache-control': 'no-cache' } });
    if (!res.ok()) throw new Error(`${url}: ${res.status()}`);
    const h = res.headers();
    console.log(attempt, {
      status: h['cf-cache-status'] ?? h['x-cache'] ?? h['cache-status'] ?? 'unknown',
      age: h['age'] ?? '0',
      pop: h['cf-ray']?.split('-')[1] ?? h['x-served-by'] ?? '-',
    });
  }
} finally {
  await api.dispose();
}

For a quick check without Node:

curl -sI https://example.com/ | grep -iE '^(cache-control|age|cf-cache-status|x-cache|cache-status):'

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

RenderScreenshot

When we designed delivery, we kept image bytes away from the API servers. The Cloudflare Worker that renders a page writes the image straight to an R2 storage bucket, and cdn.renderscreenshot.com is that bucket on a custom domain served through Cloudflare. The API server handles only metadata. Every successful response carries the public link in X-Cache-URL:

curl -s -D - -o /dev/null -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "preset": "og_card", "cache": {"ttl": 604800}}' \
  | grep -i '^x-cache'

Use the X-Cache-URL value directly in <img> or og:image tags; it needs no key. It stays available until X-Cache-Expires, which cache.ttl controls. Each fresh render gets a new link and therefore a new edge cache entry, so an old copy at the edge is never served in place of a new render. The caching reference covers TTLs and purges. This request was not run for this page; run the curl check above on the returned link to see its edge headers.

Common mistakes

  • Caching per-user images at the edge. A dashboard screenshot served with public can reach the wrong user. Mark it private or keep it off the CDN.
  • Purging the origin and not the edge. The edge keeps serving its copy.
  • Testing from one location. A hit in your region says nothing about other edges. Check from where your users are.
  • Expecting no-cache in the request to bypass the edge. Many CDNs ignore it. Purge or change the URL.
  • Unbounded query strings. Random parameters create one edge entry each and lower the hit rate. Normalize them, as with any cache key.

To get screenshots served from the edge without running your own storage, create a free account and use the X-Cache-URL link.

Frequently asked questions

What does cf-cache-status: HIT mean?
Cloudflare served the response from the edge data centre's cache without contacting the origin. MISS means it fetched from the origin, DYNAMIC means the response was not eligible for caching, and EXPIRED means a stale copy was refreshed.
Is an edge cache the same as the browser cache?
No. The browser cache is private to one user. An edge cache is shared by everyone routed to that data centre, so it must never hold per-user content unless the response is marked private or the cache key includes the user.
Do I need a CDN in front of screenshots from RenderScreenshot?
Not for the stored copy. The X-Cache-URL link is already served from cdn.renderscreenshot.com, a Cloudflare-fronted storage bucket. Add your own CDN only if you copy images to your own storage or serve them from your domain.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.