Serverless Chromium: Definition, Sizes and Trade-offs

Serverless Chromium is a compressed Chromium build that runs in functions such as AWS Lambda. How it works, measured sizes, default flags to review, and code.

By Simon Chiu · Updated

Serverless Chromium is a build of the Chromium browser packaged to run inside short-lived function platforms such as AWS Lambda or Google Cloud Functions: compressed to fit their size limits, unpacked to the temporary disk at cold start, and launched with flags that suit a single-process, sandbox-restricted environment.

The best-known package is @sparticuz/chromium. Cloudflare took a different path with Browser Rendering, where the platform runs the browser for you. This entry is part of the screenshot glossary.

What it means in plain terms

A normal Chrome install is too large for a function. The macOS Chrome for Testing build that Puppeteer downloaded on our machine took 376 MB on disk, and its headless shell 201 MB. Lambda's zip deployment limit is 250 MB unzipped, according to the AWS Lambda quotas page.

Serverless Chromium solves this by shipping only the headless shell, compressed with Brotli, plus the system libraries and fonts the function image lacks. At cold start it decompresses everything into /tmp, then your code launches it with puppeteer-core or playwright-core.

What is in the package (measured)

File in @sparticuz/chromium 153.0.0 Size
bin/chromium.br (the browser, Brotli) 67,016,039 bytes
bin/swiftshader.tar.br (software GPU) 1,637,479 bytes
bin/al2023.tar.br (Amazon Linux 2023 libraries) 1,208,350 bytes
bin/fonts.tar.br 183,738 bytes
Whole package, unpacked (npm metadata) 70,091,665 bytes
For comparison: puppeteer-core 25.14.0 / playwright-core 1.64.1 on disk 8.1 MB / 13 MB

Measured on 2026-10-15 from the installed packages. The binaries are Linux builds; we did not execute them on Lambda for this page.

The bundled fonts.tar.br is small because it holds one family: we unpacked it and found three Open Sans files (regular, bold, italic) and a fonts.conf. Pages in Chinese, Japanese or with emoji need fonts you add yourself, or the screenshot shows empty boxes.

How it works technically

sequenceDiagram
  participant L as Lambda invocation
  participant T as /tmp (ephemeral disk)
  participant C as Chromium (headless shell)
  L->>L: cold start: load handler + puppeteer-core
  L->>T: executablePath() decompresses chromium.br, libs, fonts
  L->>C: launch with chromium.args
  C-->>L: page.screenshot() bytes
  Note over L,T: warm start: /tmp/chromium already exists, skip decompression

The package also chooses launch flags for you. Read them before you render untrusted URLs. In 153.0.0 the list includes:

  • --no-sandbox and --disable-setuid-sandbox, because Lambda runs as root. The Chromium sandbox is the browser's main defence against a hostile page.
  • --disable-web-security, which turns off the same-origin policy.
  • --allow-running-insecure-content and --single-process.

Those are reasonable for capturing your own pages. For arbitrary user-submitted URLs, run each capture in an isolated function with no access to secrets or internal networks, or remove the flags you do not need.

How to set it

Puppeteer on AWS Lambda

// handler.mjs. Deploy with puppeteer-core and @sparticuz/chromium of matching versions.
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';

export const handler = async (event) => {
  const browser = await puppeteer.launch({
    executablePath: await chromium.executablePath(), // decompresses to /tmp on a cold start
    args: await puppeteer.defaultArgs({ args: chromium.args, headless: 'shell' }),
    headless: 'shell',
    defaultViewport: { width: 1200, height: 630 },
  });
  try {
    const page = await browser.newPage();
    await page.goto(event.url ?? 'https://example.com', { waitUntil: 'load', timeout: 20_000 });
    const png = await page.screenshot({ type: 'png' });
    return { statusCode: 200, isBase64Encoded: true, headers: { 'Content-Type': 'image/png' }, body: Buffer.from(png).toString('base64') };
  } finally {
    await browser.close(); // a leftover process holds memory into the next invocation
  }
};

Playwright on AWS Lambda

import chromium from '@sparticuz/chromium';
import { chromium as playwright } from 'playwright-core';

export const handler = async (event) => {
  const browser = await playwright.launch({
    executablePath: await chromium.executablePath(),
    args: chromium.args,
  });
  try {
    const page = await browser.newPage({ viewport: { width: 1200, height: 630 } });
    await page.goto(event.url ?? 'https://example.com');
    return { statusCode: 200, isBase64Encoded: true, headers: { 'Content-Type': 'image/png' }, body: (await page.screenshot()).toString('base64') };
  } finally {
    await browser.close();
  }
};

The package's README recommends at least 512 MB of function memory and suggests 1,600 MB or more. Neither handler was executed on Lambda for this page; the Puppeteer on AWS Lambda tutorial covers deployment, layers and fonts step by step.

Cloudflare Browser Rendering

On Cloudflare there is no binary to ship. You declare a browser binding in wrangler.toml and launch through it:

import puppeteer from '@cloudflare/puppeteer';

export default {
  async fetch(request, env) {
    const browser = await puppeteer.launch(env.BROWSER); // [browser] binding = "BROWSER"
    try {
      const page = await browser.newPage();
      await page.goto('https://example.com');
      return new Response(await page.screenshot(), { headers: { 'Content-Type': 'image/png' } });
    } finally {
      await browser.close();
    }
  },
};

That is the pattern our own renderer is built on: a Worker with a BROWSER binding, puppeteer.launch(env.BROWSER), and browser.close() in a finally block on every capture. The Cloudflare Browser Rendering vs Lambda comparison weighs the two platforms.

RenderScreenshot

The API is serverless Chromium you do not package, patch or match to a Puppeteer version:

curl -sS --fail "https://api.renderscreenshot.com/v1/screenshot?url=https://example.com&preset=og_card" \
  -H "Authorization: Bearer $RS_API_KEY" -o card.png

Common mistakes

  • Mismatched versions. puppeteer-core and the Chromium build must agree. Upgrade them together and test.
  • Installing full puppeteer. It downloads a desktop Chrome into your bundle. Use puppeteer-core.
  • Not closing the browser. A warm instance keeps the process, and the next invocation runs out of memory.
  • Too little memory. Below the recommended size, launches time out or pages crash.
  • Assuming fonts. Add the fonts your pages need to the deployment, or captures show tofu boxes.

To capture pages without packaging a browser, get a free API key.

Frequently asked questions

Which @sparticuz/chromium version do I need?
The one whose major version matches a Chromium release your puppeteer-core or playwright-core supports. On 2026-10-15 the newest @sparticuz/chromium was 153.0.0, while puppeteer 25.14.0 shipped with Chrome 155, so the newest Puppeteer and the newest Lambda build did not match. Pin puppeteer-core to a release made for Chrome 153 and test the pair.
Why is the first request to my screenshot function so slow?
On a cold start the function must decompress Chromium into /tmp before it can launch it. Warm invocations reuse the extracted binary, so only the first request in each new instance pays that cost.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.