Automation

How to Deploy Playwright on AWS Lambda

Layer, container image, or hosted browsers.

TL;DR

  • Lambda basics. Running Playwright on AWS Lambda means pairing playwright-core with a Lambda-compatible Chromium build, @sparticuz/chromium, because Lambda ships no browser and caps a function plus its layers at 250 MB unzipped.
  • Layer or image. A zip layer is the quicker route for one small function. A container image, which can be up to 10 GB, suits a pinned browser version or extra system libraries.
  • A working handler. The Node.js 22 handler in this guide screenshots any URL to S3, closes the browser on success and failure, and comes with fixes for the errors a first deploy usually throws.
  • Skip packaging. Connect the same handler to hosted browsers with chromium.connectOverCDP(), so the function ships only playwright-core (about 14 MB) and no Chromium layer.

Introduction

Playwright on your laptop is one install and a launch() call, but an AWS Lambda Playwright setup takes more work. The function has no browser, a read-only filesystem outside /tmp, and a hard cap on how much code and binaries you can ship with it.

You'll package Chromium two ways, as a zip layer and as a container image, so you can go with whichever matches how you already deploy. Along the way, you'll write a Node.js handler that saves a screenshot to S3 and fix the launch errors a first deploy usually hits. The last section shows how to skip packaging entirely by pointing the same handler at a hosted browser.

How to use AWS Lambda and Playwright for browser automation

The example is deliberately boring. The interesting part is getting a browser to run in Lambda at all. You hand the function a URL, it opens the page with playwright-core and a Lambda-friendly build of headless Chrome, and drops a screenshot into your S3 bucket.

Once that works, swap the screenshot for whatever you actually need to run, like a scraper that has to target elements on a page, or a scheduled check. The Lambda side stays exactly as it is. Scaling takes little thought either: every concurrent call gets its own isolated environment, at least until you hit your account's concurrency limit.

Architecture overview

A trigger hands the function a URL. That can be a test event, an API call, or a schedule when you want the function triggered automatically. Inside the handler, Playwright runs against Chromium. S3 handles storing Playwright screenshots, so the function itself keeps no state.

The trigger and the bucket are standard AWS services, and the AWS integration between them is boilerplate. The one choice that shapes your setup is where Chromium lives: packaged into the function as a layer or container image, or kept out of it entirely on a hosted browser.

Playwright on AWS Lambda architecture: a test event triggers a Node.js 22 function running playwright-core and @sparticuz/chromium, which uploads a screenshot to Amazon S3

Setting up Playwright on AWS Lambda yourself

You can ship Playwright and Chromium as a zip file attached as a Lambda layer, or bake them into a Docker container image. The layer is quicker to set up, and since Chromium lives there, the function's own code stays small enough to edit in the Lambda console. The image takes a bit more work, but at up to 10 GB it has room for fonts, extra system libraries, or a pinned Chromium version.

Flowchart for choosing how to run Playwright on AWS Lambda: hosted browsers, a container image, or a Lambda layer with @sparticuz/chromium

Whichever you pick, set the function's architecture to x86_64. The @sparticuz/chromium npm package only ships x64 binaries, so an arm64 function can't launch it.

Creating a ZIP library file

A layer is just a zip with a specific folder layout. Getting there takes three steps.

  1. Create a layer folder with a Lambda-compatible Chromium build and playwright-core inside it.
  2. Zip the folder in the structure AWS Lambda expects for a Node.js layer.
  3. Upload the zip to an S3 bucket, then publish it as a layer.

For the browser, use @sparticuz/chromium, a compressed Chromium build made for serverless platforms. If an older tutorial pointed you at chrome-aws-lambda, skip it: @sparticuz/chromium is the maintained replacement. You need a build like this because playwright-core doesn't download Playwright browsers and only drives the one you point it at.

Run the build on a Linux machine, like an EC2 instance or a CI runner. If your local machine is a Mac, run it inside a node:22 Docker container with --platform linux/amd64. The script handles the first two steps, creating the nodejs folder, installing both packages into it, and zipping the result.

#!/bin/bash

# Lambda Node.js layers must contain a top-level "nodejs" folder
mkdir nodejs && cd nodejs

# Install the Chromium build for Lambda, plus playwright-core, pinned to
# the same versions as package.json (not the full "playwright" package,
# which adds the test runner on top of playwright-core)
npm install "@sparticuz/chromium@153.0.0" "playwright-core@1.64.1"

# Zip the nodejs folder - this is the file you upload as the layer
cd ..
zip -r combined_libs.zip nodejs

Watch the size when you upload. The zip comes out at about 75 MB, which is over the 50 MB limit for a direct upload, so put it in an S3 bucket and create the layer from there. Unzipped it's about 87 MB, comfortably inside Lambda's 250 MB cap for a function plus its layers.

aws s3 cp combined_libs.zip s3://your-bucket/
aws lambda publish-layer-version --layer-name playwright-chromium \
  --content S3Bucket=your-bucket,S3Key=combined_libs.zip \
  --compatible-runtimes nodejs22.x --compatible-architectures x86_64

Using a container image instead

If your team already ships Lambdas as containers, this route will feel more familiar. Build on AWS's Lambda base image for Node.js 22 and install the same packages into it. The Dockerfile copies in package.json and index.mjs, so write both first (see Writing the Lambda function below), then come back to build and push the image.

# AWS's Lambda base image already includes the runtime interface client
FROM public.ecr.aws/lambda/nodejs:22

# Install playwright-core and @sparticuz/chromium from package.json
COPY package*.json ${LAMBDA_TASK_ROOT}/
RUN npm install --omit=dev

COPY index.mjs ${LAMBDA_TASK_ROOT}/

CMD ["index.handler"]

Keep the --provenance=false flag in the build command. Lambda rejects the image without it.

docker buildx build --platform linux/amd64 --provenance=false -t playwright-lambda .

Before you push it, you can test the Docker image locally. The AWS base image ships with the Lambda Runtime Interface Emulator, so run the container with your bucket name and AWS credentials, then send it an event from a second terminal.

docker run --platform linux/amd64 -p 9000:8080 \
  -e BUCKET_NAME=your-bucket -e AWS_REGION \
  -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
  playwright-lambda

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
  -d '{"url":"https://www.browserless.io"}'

Then push the image to an Amazon ECR repository so you can create the function from it.

Writing the Lambda function

Create a new Lambda function in your AWS account, from the AWS console or with the AWS CLI. On the layer route, pick the Node.js 22.x runtime or later. On the container route, choose Container image and point it at the image you pushed to ECR.

Lambda's defaults assume a small function, and Chromium is anything but, so change three settings in the Lambda function configuration before the first run, or your first test will fail for reasons that have nothing to do with your code.

  • Memory. Give it at least 1,600 MB, which is what the @sparticuz/chromium maintainers recommend. Lambda allocates CPU in proportion to memory, so you get a faster Chromium launch too.
  • Timeout. The default 3 seconds won't cover a cold Chromium launch plus a page load, so set it to a minute or more.
  • Bucket access. Add a BUCKET_NAME environment variable, and give the function's execution role s3:PutObject permission on that bucket.

Now write the handler and save it as index.mjs. @sparticuz/chromium is published as an ES module, so import syntax is the natural fit, and the .mjs extension tells Lambda to load the file as one. The same file works unchanged on the layer and the container image.

The handler is an async function that reads a URL from the event, launches Chromium with the binary and launch args from @sparticuz/chromium, takes a screenshot, and uploads the PNG to your bucket.

// index.mjs
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import chromium from "@sparticuz/chromium";
// Playwright's own chromium export is renamed so it doesn't clash
// with the @sparticuz/chromium import above
import { chromium as playwright } from "playwright-core";

const s3 = new S3Client({});

export const handler = async (event) => {
  const bucketName = process.env.BUCKET_NAME;

  // Use the URL from the test event, or fall back to example.com
  const url = event.url || "https://example.com";
  const s3Key = `screenshots/${new URL(url).hostname}.png`;

  const browser = await playwright.launch({
    args: chromium.args,
    executablePath: await chromium.executablePath(),
  });

  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: "load" });
    const screenshot = await page.screenshot();

    await s3.send(
      new PutObjectCommand({
        Bucket: bucketName,
        Key: s3Key,
        Body: screenshot,
        ContentType: "image/png",
      }),
    );

    console.log(`Screenshot uploaded to s3://${bucketName}/${s3Key}`);
  } finally {
    await browser.close();
  }
};

Don't skip the finally block and its await browser.close(). Lambda reuses execution environments on warm starts, and a page that fails to load would otherwise leave Chromium running for the next invocation to trip over. Errors still propagate, so a failed run shows up in the Lambda console and in CloudWatch Logs.

Runtime versions and dependencies

Node.js 22 is the minimum because Node.js 20 hit its Lambda deprecation date on April 30, 2026, and @sparticuz/chromium 153 needs Node.js 22.17 or newer. On the container route, the AWS base image in the Dockerfile already covers both.

The handler imports three packages, so list them in a package.json next to index.mjs. The container build installs from this file, and the layer script pins the same versions, which keeps the two routes from drifting apart.

{
  "dependencies": {
    "@aws-sdk/client-s3": "^3.1154.0",
    "@sparticuz/chromium": "153.0.0",
    "playwright-core": "1.64.1"
  }
}

Those were the current versions in October 2026. All three release often, so check npm before you deploy and update the layer script to match. Lambda's Node.js runtimes already bundle @aws-sdk/client-s3, so you can drop it from the file if you don't need it pinned.

Take upgrades to @sparticuz/chromium slowly, because its versions track Chromium's major version, and the maintainers warn that breaking changes can land in a patch release. Unlike Puppeteer on AWS Lambda, Playwright doesn't publish a list of matching Chromium versions, but its docs only guarantee the Chromium build that ships with each release. These versions ran cleanly together in testing for this guide, so when you upgrade, bump both packages at once and rerun the function before it goes anywhere near production. On the hosted route, connectOverCDP() is generally compatible across Playwright versions, so your playwright-core usually doesn't have to match the one Browserless runs (see version compatibility).

Add layers to AWS Lambda

On the layer route, attach the layer you created to your function under Layers in the console. If you went with a container image, skip this step. The image already has everything the function needs.

AWS walks through the console steps in its guides to creating and deleting layers and adding layers to functions.

Trigger and test the Playwright script

Time to find out if it works, so open the function's Test tab, create an event with a url field, and run it.

{ "url": "https://www.browserless.io" }

If it works, the log shows a success message with the S3 location, and the screenshot is waiting under screenshots/ in your bucket.

If the test fails, check these four first. They're the errors a first deploy tends to hit.

  • Cannot find package '@sparticuz/chromium'. The layer isn't attached, or the zip is missing its top-level nodejs folder. On the container route, check that npm install ran during the image build.
  • Chromium fails to launch. Make sure the function runs on x86_64 and that you pass chromium.args to launch() as is. Those args include --no-sandbox and --single-process, which Chromium needs inside Lambda's runtime environment.
  • Timeouts or out-of-memory errors. Raise memory first. A slow launch usually means the function is short on CPU as well.
  • ERR_INSUFFICIENT_RESOURCES after many warm runs. Playwright deletes its temporary profile folder in /tmp when browser.close() runs, so /tmp only fills up when an invocation ends before the finally block, usually because the function hit its timeout. Raise the timeout, or raise the function's ephemeral storage above the 512 MB default. Don't pass --user-data-dir in the launch args: Playwright's launch() rejects it.

Connect to hosted browsers instead

The catch with packaging Chromium yourself is that it's now yours to maintain, from rebuilding the layer on every Chromium release to the memory and crash issues of running Chrome on AWS. If that's not a job you want, Browserless runs hosted browsers you connect to over WebSocket. Your function then ships only playwright-core, about 14 MB installed, and no Chromium layer at all.

Integrating Playwright with a hosted browser takes two small changes to index.mjs. To connect Playwright to Browserless, drop the @sparticuz/chromium import and swap the launch() call for a connection. With that import gone, you can use Playwright's chromium export under its own name. Remove @sparticuz/chromium from package.json too. Then rebuild the image on the container route. On the layer route, the layer also holds playwright-core, so don't just detach it. Rebuild the layer with only playwright-core, or install playwright-core into the function package before you detach the layer.

import { chromium } from "playwright-core";

// Replaces playwright.launch(), so the browser runs on Browserless, not in Lambda
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/chromium?token=${process.env.BROWSERLESS_TOKEN}`,
);

Put your API token in a BROWSERLESS_TOKEN environment variable, and swap production-sfo for production-lon or production-ams if your function runs in Europe. Everything else in the handler, from await browser.newPage() to the S3 upload, stays the same, so the same Playwright script runs unchanged. You can also turn the function's memory back down. Playwright executes against a remote browser, so Chromium isn't running inside the function anymore.

If all the function does is capture a page, the Screenshot API returns the PNG from a single POST /screenshot request, so you don't need playwright-core either.

Conclusion

The packaging choice matters less than it looks. A layer and a container image run the same index.mjs. You can start small with a layer and switch to an image when you need system libraries or a pinned Chromium, without touching your Playwright code.

What doesn't go away on either route is maintaining the browser itself. Browserless runs that part for you, leaving your function with only playwright-core and a WebSocket URL. To try it with the handler from this guide, sign up for Browserless.

AWS Lambda Playwright FAQs

Why doesn't chrome-aws-lambda work for Playwright on Lambda anymore?

chrome-aws-lambda stopped receiving releases in 2021, so its Chromium build is years behind what current playwright-core versions expect. @sparticuz/chromium grew out of a fork of it and is the maintained replacement.

Should I use a Lambda layer or a container image for Playwright?

A layer can be shared across several functions and updated on its own, which is handy when a few small functions all need Chromium. A container image bundles your code, Chromium and any system libraries into one versioned artifact per function, so the entire setup is easier to pin and roll back, with no environment drift between deploys.

Can I run a Playwright project's automated tests on AWS Lambda?

It works for small checks, like a function that can run Playwright scripts on a schedule or after a deploy and report which tests pass or fail. That covers automated web testing at a small scale. A full Playwright project, with the playwright test runner, parallel execution across workers, its reports and rich debugging tools like the trace viewer, is better off on a CI runner where you run tests in parallel. See Playwright testing for scaling a test suite, and choosing CI runners for Playwright for the runner side.

Can I run Chromium, Firefox and WebKit on Lambda?

Playwright supports Chromium, Firefox and WebKit, but @sparticuz/chromium only packages Chromium, so the setup in this guide is Chromium-only. To test on multiple browsers from Lambda, connect to Browserless's hosted Firefox or WebKit with firefox.connect() or webkit.connect() on the /firefox/playwright and /webkit/playwright endpoints, which gives you cross browser support without packaging more browsers. Unlike the connectOverCDP() call mentioned earlier, connect() is tied to the Playwright version, so check that your playwright-core release is one Browserless supports.

Can I run Playwright on an EC2 instance instead of Lambda?

Yes, and EC2 gives you a persistent machine with no execution-time limit, so it's the better home for long-running or high-volume automation that would run into Lambda's 15-minute maximum. Lambda is the better fit for occasional or event-triggered work, like a screenshot on demand, because of the billing model AWS Lambda offers, where you pay per request and for the time the function runs, with nothing charged while it sits idle. Light use can even fit inside the AWS Free Tier's monthly Lambda allowance.

How does Lambda fit into a Playwright test pipeline?

One way to wire it up keeps your code in a GitHub repository, so when you push code, the pipeline detects the change and runs the full test suite with npx playwright test. In AWS CodeBuild, Playwright installs the way it does on any Linux build machine, so you don't need the Lambda packaging from this guide there.

Lambda earns its place after the deploy stage, as a smoke check that can execute Playwright scripts against the live site. With that feedback loop, a failed check tells you within minutes that a bug fix broke something the test suite didn't cover, and the pass and fail history helps you monitor build health over time.

Is Playwright on Lambda a replacement for manual testing?

It covers the repeatable checks, while manual testing still catches the problems nobody thought to script. Start with a small sample test, like a test user logging in on an open source demo app, before you point it at your own site. The basic structure matches the screenshot handler, with assertions where the S3 upload was, so a basic understanding of Playwright's API is enough. The usual conclusion, manual testing for exploration and scripts for repetition, holds here too, and the testing process speeds up as more of the repetition moves to Lambda.