AWS Builder Center

Case Study: Playwright Survival on AWS Lambda

Can Playwright run reliably inside a serverless container, or does the "serverless everything" trend break down the moment you introduce a full browser engine?

Software Engineer - St. Pölten
Headless browser automation and serverless compute don't naturally get along. A browser like Chromium expects a real Linux desktop environment underneath it. Lambda adds its own constraints on top: a read-only filesystem outside  /tmp , tight memory ceilings, and a runtime with 300+ MB browser binaries in mind.

The Experiment: Flip the Base Image

The hypothesis to test next: instead of forcing AWS tooling onto a generic image, start from AWS's own Lambda Node.js base (Amazon Linux 2023) and add only the browser dependencies Chromium needs. This flips the usual dependency direction. Amazon Linux already speaks Lambda natively, so the only unknown is whether  dnf  has what Playwright requires.

Setup

mkdir playwright-lambda && cd playwright-lambda
npm init -y
npm install playwright

The Handler

The handler code needed one adjustment: Chromium's default launch flags assume a sandboxed desktop process model that Lambda's container simply doesn't support. Without disabling the sandbox and forcing single-process mode, the browser process was killed before  launch()  even resolved.
const { chromium } = require('playwright');

exports.handler = async (event, context) => {
let browser = null;

try {
console.log("Launching browser...");
browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--single-process', // Required for Lambda
'--disable-gpu'
]
});

const context = await browser.newContext();
const page = await context.newPage();

const targetUrl = event.url || '<url>';
console.log(`Navigating to ${targetUrl}`);

await page.goto(targetUrl, { waitUntil: 'networkidle' });

const pageTitle = await page.title();

const metaDescription = await page.locator('meta[name="description"]').getAttribute('content').catch(() => null);

const topLinks = await page.evaluate(() => {
return Array.from(document.querySelectorAll('a'))
.map(a => ({ text: a.innerText.trim(), href: a.href }))
.filter(link => link.text && link.href && link.href.startsWith('http'))
.slice(0, 5);
});

// Capture a screenshot (encoded as base64 to return in JSON)
// We use JPEG with lower quality to keep the payload size manageable for Lambda
const screenshotBuffer = await page.screenshot({ type: 'jpeg', quality: 60 });
const screenshotBase64 = screenshotBuffer.toString('base64');

console.log(`Successfully scraped: ${pageTitle}`);

return {
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
success: true,
url: targetUrl,
title: pageTitle,
description: metaDescription,
topLinks: topLinks,
screenshotBase64: screenshotBase64
}),
};

} catch (error) {
console.error("Error during execution:", error);
return {
statusCode: 500,
body: JSON.stringify({ success: false, error: error.message })
};
} finally {
if (browser) {
await browser.close();
}
}
};

The Dockerfile

This is where the experiment's core hypothesis gets tested, does  dnf install  actually resolve every library Chromium needs?
# Use the official AWS Lambda Node.js 20 base image
FROM image/lambda/nodejs:20

# Install system dependencies required by Playwright in Amazon Linux 2023
RUN dnf install -y \
atk at-spi2-atk at-spi2-core alsa-lib cups-libs \
gtk3 libdrm mesa-libgbm libxkbcommon libXcomposite \
libXcursor libXdamage libXext libXi libXrandr \
libXScrnSaver libXtst nss nspr pango

COPY package.json index.js ./
RUN npm install

ENV PLAYWRIGHT_BROWSERS_PATH=/var/task/ms-playwright
ENV PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true

RUN npx playwright install chromium

CMD ["index.handler"]
Answer: yes, with one caveat.  PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS  has to be set explicitly, because Playwright's own host-check script assumes Ubuntu and will refuse to run on Amazon Linux otherwise, even when every actual library is present. That single environment variable was the difference between a working image and a false "unsupported OS" failure.

Local Testing (Recommended)

docker run -p 9000:8080 playwright-lambda-image:latest
Using the standard HTTP endpoint used by the AWS Lambda Runtime Interface Emulator (RIE), we can test the function locally.
When we run the Lambda container locally (e.g., using the  docker run  command), AWS uses this emulator inside the container to mimic the real AWS Lambda environment. It spins up a local web server (on port 8080, mapped to our local port 9000) so we can test the function using standard HTTP requests.
curl -XPOST "<container:port>/2015-03-31/functions/function/invocations" \
-H "Content-Type: application/json" \
-d '{"url":"url"}'
Here is the breakdown of what each part of that URL means:
  •  /2015-03-31/ : The API version. AWS typically uses the release date of an API as its version number. March 31, 2015, is the date AWS officially released the Lambda Invoke API
  •  /functions/function/ : The path specifying which function to invoke. Because we are just testing a single local container, the emulator doesn't care about the function's actual name, so it just uses the placeholder name "function".
  •  /invocations : The specific API action being called. It tells the AWS runtime to trigger the handler function inside the code and pass the payload to it.

Next Steps

 index.js  is the heart of this Lambda function. Since the infrastructure (Chromium, dependencies, Docker) is now fully working,  index.js  is exactly where the actual automation or scraping logic should happen.

Integrating with the Amazon Ecosystem

Once the local scraping logic is complete, we can leverage other AWS services to build a fully automated, scalable pipeline:
  1. Amazon ECR (Elastic Container Registry) Push the finished Docker image to an ECR repository. Lambda requires container images to be hosted in ECR before it can deploy them.
  1. AWS Lambda Configuration Deploy the function using the ECR image. Crucial step: By default, Lambda allocates 128 MB of memory. Headless Chromium will crash instantly. We must go to the function's General Configuration and increase the Memory to at least 2048 MB and the Timeout to at least 1 minute.
  1. Amazon S3 (Simple Storage Service) Instead of returning massive base64 strings in the HTTP response, we can update  index.js  using the AWS SDK ( @aws-sdk/client-s3 ) to upload the captured screenshots or scraped JSON data directly to an S3 bucket for persistent storage.
  1. Amazon EventBridge (formerly CloudWatch Events) If we want our scraper to run on a schedule (e.g., scrape a competitor's pricing page every morning at 8 AM), we can create an EventBridge Rule with a Cron expression to automatically trigger our Lambda function.
  1. Amazon API Gateway / Lambda Function URLs If we want to expose our scraper as a public HTTP API that our frontend or other applications can call over the web, we can attach an API Gateway or generate a direct Function URL for our Lambda.

Conclusion

We have successfully built and containerized a Playwright-based web scraper on AWS Lambda using Amazon Linux 2023. The initial hypothesis that we could run a full headless browser inside a minimal Lambda environment by simply installing the correct system dependencies was proven correct.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article