This site is not affiliated with or endorsed by Cloudflare, Inc. It showcases deployable experiments across most Cloudflare products.

Blog20 min read

How to take a screenshot of any URL at the edge

A deep guide to capturing PNG screenshots with Cloudflare Browser Rendering on Workers - architecture, bindings, waits, caching, auth, pitfalls, and a deployable Screenshot API experiment.

MacBook on a desk showing lines of code on the screen
Photo by Christopher Gower on Unsplash

Marketing sites, QA bots, social previews, and agent toolchains all need a reliable answer to one question: what does this URL look like right now? Running Chromium yourself means managing browsers, scaling screenshots, and paying for idle capacity. Cloudflare Browser Rendering runs headless Chrome on Cloudflare’s network so a Worker can open a page and return a PNG without operating a browser farm.

This guide goes deep on architecture, product choices, waits, caching, auth, and failure modes - then points you at the Screenshot API experiment as a tested starting point.

Cloudflare products in this pattern

ProductRoleDocs
WorkersHTTP API, validation, response streamingRuntime + routing
Browser RenderingHeadless Chromium at the edgeBinding + REST options
@cloudflare/puppeteerDrive navigation, viewport, screenshotPuppeteer API on Workers
Workers bindingsWire browser into envwrangler.json
R2 (optional)Persist hot screenshotsObject storage
Cache API (optional)Short-TTL HIT/MISS for identical URLscaches.default

Workers remain the orchestration layer. Browser Rendering is the product that actually paints pixels. Everything else is optional hardening.

Why screenshots belong at the edge

A screenshot path is usually:

  1. Accept a target URL
  2. Launch or reuse a browser session
  3. Navigate and wait for a usable paint
  4. Capture a viewport (or full page)
  5. Return image bytes - or upload to object storage

Steps 2–4 dominate cost and failure rate if you own the browser. Browser Rendering moves that into Cloudflare’s platform. Your Worker stays thin: validate → call the binding → stream or store the result.

Good fits

  • Open Graph / link-preview pipelines when og:image is missing or stale
  • Visual regression hooks for staging URLs
  • Support tools (“show me what the customer sees”)
  • Agent tools that need pixels, not just HTML

Poor fits

  • Capturing authenticated apps without a deliberate login strategy
  • Pixel-perfect print layouts (prefer PDF generation via Browser Rendering / the PDF API experiment)
  • Scraping private intranet URLs Cloudflare cannot reach

If you only need title / Open Graph tags from static HTML, prefer HTMLRewriter via Metadata Extractor - it is cheaper than launching Chromium.

End-to-end architecture

Client
  → Worker (validate url, auth, rate limit)
    → Browser Rendering (navigate + screenshot)
      → PNG bytes
        → optional R2 put / Cache API put
          → Response image/png or JSON error

Keep the public contract boring so clients and agents stay simple:

ConcernRecommendation
Inputurl query/body, http/https only
Outputimage/png on success; JSON { error, code } on failure
TimeoutsFail before the Worker’s practical limit (~60s for this catalog)
AuthAdd before public exposure (Access, signed URLs, or secrets)
AbuseRate limits + allowlists for destination hosts when possible

The Screenshot API experiment follows that shape: single responsibility, typed env, route-level validation before the browser is touched.

Example once deployed:

curl -fsSL "https://<your-worker>/screenshot?url=https://example.com" \
  -o example.png

Invalid schemes or missing params should return 4xx JSON, not a truncated PNG.

Bindings and Wrangler setup

Browser Rendering is declared as a Worker binding (see Browser Rendering / Workers binding). Conceptually:

// wrangler.json (shape varies by experiment version - verify in the package)
{
  "browser": {
    "binding": "BROWSER"
  }
}

Type the binding in src/types/env.d.ts, then call it from a route after validation. Prefer the experiment’s lib helpers over inventing a new Puppeteer wrapper on day one - Wrangler + the binding is already the hard part for first deploy.

Locally, follow Browser Rendering’s local development notes in the product docs; some browser features behave differently in wrangler vs production. Always verify a real deploy before committing to an SLA.

Wait for the right moment

SPAs and marketing pages often paint late. Waiting only for load can capture empty shells. The @cloudflare/puppeteer docs cover wait strategies (networkidle, selectors, timeouts). When you fork the experiment:

  • Prefer a selector that marks “content ready” for your target class of sites
  • Cap total wait time so one slow origin cannot pin your Worker
  • Treat timeouts as structured FETCH_ERROR / BROWSER_TIMEOUT style codes

Cap what you render

Unbounded full-page captures blow up memory and time. Start with a fixed viewport (for example 1280×720) and only enable full-page when the product truly needs scroll height.

Block noise when you control the page

Heavy third-party tags inflate time-to-screenshot. Where allowed, block known tracker domains or wait past cookie banners with a selector. Keep that logic product-specific - do not bake aggressive blocking into a generic public API without disclosure.

Caching and persistence

Identical marketing URLs are requested often. Two complementary Cloudflare products help:

  1. Cache API - short TTL for identical cache keys (URL + viewport + wait mode). See Edge Cache.
  2. R2 - durable objects for screenshots you want to reuse or serve later. See R2 Storage.

A practical policy:

  • Cache key: sha256(url + viewport + fullPage flag)
  • TTL: minutes for volatile pages; hours for rarely changing marketing pages
  • On HIT, skip Browser Rendering entirely
  • On MISS, screenshot → optionally R2.put → cache.put → return

Never cache personalized or authenticated captures under a shared key.

Auth, abuse, and cost control

Browser Rendering is metered. A public unauthenticated screenshot endpoint will be scraped.

Hardening ideas that map cleanly onto Cloudflare:

  • Cloudflare Access / service tokens for internal tools
  • HMAC-signed URLs with short expiry for partner integrations
  • Rate Limiting or a Worker-side limiter (Rate Limiter Demo)
  • Destination allowlists (only your domains) for preview generators
  • Max concurrent browser sessions per customer

Log colo from request.cf when debugging regional failures - see Where Am I.

Once screenshots work, nearby patterns unlock richer tools without reinventing Chromium:

GoalExperimentProduct focus
PNG of a URLScreenshot APIBrowser Rendering
Links after JSBrowser LinksBrowser Rendering
Rendered → markdownBrowser Markdown ScrapeBrowser Rendering
Print-style PDFPDF APIBrowser Rendering
CDP inspectBrowser CDP InspectBrowser Rendering + CDP

Common failure modes

SymptomLikely causeMitigation
Blank PNGWaited too early / SPA shellStronger wait; Browser Rendering wait APIs
TimeoutSlow origin or heavy pageLower viewport; block assets; fail fast
INVALID_URLBad scheme or empty inputShared validateUrl helper
Auth wallsLogin requiredSession cookies / dedicated auth flow
Flaky fontsCDN font latencyLonger wait or self-host fonts on target

Deploy the reference

  1. Open Screenshot API
  2. Read the official Browser Rendering binding docs once
  3. One-click Deploy (or clone apps/experiments/screenshot-api and use Wrangler)
  4. Confirm the browser binding in the Cloudflare dashboard
  5. Hit /screenshot?url=https://example.com and save the PNG
  6. Layer Cache API / R2 / auth before exposing publicly

You get a tested Worker with clear error codes instead of a blank Puppeteer template. Fork the lib helpers into your app when the experiment’s shape matches your needs - and keep Browser Rendering as the named product capability, with Workers as the vehicle that wires it.

End-to-end checklist before production

  1. Confirm the Browser Rendering binding is present in the dashboard and wrangler.json.
  2. Decide viewport defaults (width, height, device scale) and document them in your API.
  3. Pick a wait strategy per page class (marketing vs SPA vs docs).
  4. Add destination allowlists or signed URLs before opening the endpoint publicly.
  5. Cache identical captures with the Cache API or persist to R2.
  6. Emit structured metrics: latency, timeout rate, colo, destination host.
  7. Load-test concurrent captures - Browser Rendering capacity is finite.
  8. Document retention for stored PNGs (privacy + cost).

FAQ

Do I need Puppeteer locally?

For learning, deploy the experiment and call it remotely. Local Browser Rendering support depends on current Wrangler + product docs - always verify against the live binding.

Can I screenshot authenticated pages?

Yes, but you must supply cookies/headers deliberately and never log secrets. Treat auth captures as sensitive data.

Screenshot vs PDF?

Use PNG for previews and visual QA. Use PDF API / Browser Rendering PDF when print layout matters.

What about full-page captures?

They are slower and heavier. Default to viewport; enable full-page only when the product requires scroll height.

Related experiments

These deployable references implement the patterns in this article. Open the docs for API details, then Deploy or clone the source.

  • Screenshot API - Capture website screenshots with Cloudflare Browser Rendering - full-page or viewport PNG/JPEG screenshot API
  • Browser Links - Extract all links from JavaScript-rendered pages using Cloudflare Browser Rendering - SPA-aware link discovery for crawlers
  • Browser Markdown Scrape - Convert any URL to Markdown or scrape CSS with Browser Rendering quickAction - JS-rendered pages to clean text for LLMs
  • PDF API - Generate PDFs from any webpage with Cloudflare Browser Rendering - HTML/URL to PDF API at the edge
  • R2 Storage API - Upload and serve files with Cloudflare R2 - private and public bucket patterns, object CRUD, and edge file storage reference
  • Edge Cache - Fetch and cache URLs with the Workers Cache API at the edge - Cache API reference pattern for Cloudflare Workers

Cloudflare Experiments is an independent open-source catalog - not affiliated with Cloudflare, Inc.