Web capture engineering

Running Playwright screenshots in serverless functions

The screenshot works locally, then the function cannot find Chromium, exceeds its compressed bundle limit, times out during launch or dies when two browser contexts overlap.

3 min readPublished Updated
What you actually see

Intermittent 5xx responses around deploys and traffic spikes, with most of the request budget spent before `page.goto()` starts.

Reproduce it

  1. 01Log four timings separately: module import, browser launch, navigation and encoding.
  2. 02Run the function from a cold deployment image rather than a warm local process.
  3. 03Invoke it concurrently at the platform limit and record peak resident memory, not only duration of one successful request.
The DIY version

Start with the smallest thing that works

Start with one browser per invocation because it is easier to reason about. Reuse introduces lifecycle and isolation decisions that should be measured, not assumed.

route.ts
import { chromium } from "playwright-core";

export async function POST(request: Request) {
  const { url } = await request.json();
  const browser = await chromium.launch({ headless: true });
  try {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    await page.goto(url, { waitUntil: "load", timeout: 30_000 });
    const image = await page.screenshot({ type: "png" });
    return new Response(image, { headers: { "content-type": "image/png" } });
  } finally {
    await browser.close();
  }
}
Where it breaks

The call is small. Everything around it is the system.

The executable and package disagree

Playwright versions are coupled to browser revisions. A runtime layer that silently updates one side can turn a deploy into a launch failure.

Cold start consumes the caller timeout

Downloading, unpacking or launching Chromium happens before useful navigation. Raising the page timeout does not extend the platform request lifetime.

Concurrency multiplies memory

A single successful invocation says little about two overlapping browsers. Platform concurrency and in-process contexts must share a deliberate capacity budget.

Reuse crosses trust boundaries

Keeping a browser warm helps latency, but contexts, downloads, service workers and credentials need explicit per-job isolation and cleanup.

Keep the DIY version when

  • The platform provides a supported browser image and the function captures only application-owned URLs.
  • Traffic is bounded, artifacts are returned synchronously, and the team already observes cold starts and memory saturation.
The recipe

Playwright screenshot in serverless

Move the browser lifetime outside the function. The caller keeps a short HTTP request, while the render plane owns the compatible binary, isolation and bounded execution.

Capture settings
Enter the source, choose your options, then run the capture.
Result
Your capture will appear here and stay in view.
waiting

No result yet

Complete the settings and run the tool. Images, PDFs, text, and video all preview in this panel.

Send this exact request
cURL, TypeScript and Python are generated from the same configuration as the demo.
curl --fail-with-body "https://api.pagecapture.dev/v1/take" \
  -H "X-Access-Key: $PAGECAPTURE_KEY" \
  -H "Content-Type: application/json" \
  --data '{"url":"https://example.com","format":"png","viewport_width":1280,"viewport_height":720}' \
  -o capture.png
What you get, and what it costs

Output, limits, cost, failures

Output

A PNG response from a pinned Chromium engine. For a caller with a shorter request window, the same request can use async delivery and a signed webhook.

Cost

One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.

1 creditper successful non-cached capture

Limits

  • The initial render engine is Chromium; keep Playwright for Firefox or WebKit requirements.
  • The total PageCapture timeout is 90 seconds and navigation is capped at 30 seconds.
  • Arbitrary CDP sessions and unrestricted Node callbacks are intentionally outside the HTTP contract.

Common errors

The render pool has no safe capacity for a new job; use `Retry-After`.

HTTP 503 · retryable

timeout_errornever billed

Navigation or the page readiness rule exceeds its declared budget.

HTTP 500 · retryable

Try it against your own page

The runner above carries this exact configuration into the playground — no retyping, no starting over.

Return to the configured runner
Tested and reviewed

Written by PageCapture Engineering. Reviewed by PageCapture runtime maintainers. Verified 2026-08-12 by running the published code against a fixture.

The recipe is normalized and all generated language variants are syntax-checked; browser-version and timeout claims come from the installed Playwright contract and PageCapture parameter inventory.

engine-crunknown-pwunknown-f2026-07-1-b2026-07-1StatusCapture API quickstart referenceEditorial method

Sources and verification basis

Sources support the browser and API behaviors named above. PageCapture-specific limits and billing are taken from the public contract; external sources are used for the underlying browser behavior.

  1. BrowsersPlaywright

    Documents browser installation and version coupling.

  2. DockerPlaywright

    Documents the supported container model and security considerations.

  3. Defines the API timeout and readiness contract.