Running headless Chrome in Lambda is painful. Here's the clean alternative that actually works.
AWS Lambda functions have a 250MB deployment package limit (50MB zipped). Chromium — the browser engine used by Puppeteer and Playwright — weighs approximately 300MB unzipped. This immediately disqualifies running a standard Chromium binary in Lambda. The workarounds are painful: chrome-aws-lambda and @sparticuz/chromium are stripped-down Chromium builds compressed to fit Lambda's limits, but they lag behind Chromium releases by weeks or months, have different rendering behavior than standard Chrome, and frequently break on Lambda runtime updates.
Even when you get Puppeteer running on Lambda, the operational overhead continues. Cold starts take 3–8 seconds as Chromium initializes. Memory consumption pushes Lambda functions toward 1GB+ configurations. Concurrent invocations mean concurrent browser processes, each consuming memory independently. Browser process cleanup becomes a concern — Lambda's execution environment may reuse across invocations, but browser processes do not always clean up cleanly.
SnapAPI runs Chromium externally. From your Lambda function, screenshot generation is a single HTTPS request to our API. Your Lambda function stays small (the API call requires no additional libraries beyond what Node.js or Python provide by default). Cold starts are fast because there is no browser to initialize. Memory consumption is minimal. Concurrent invocations each make an independent HTTP request — no shared browser state, no process cleanup to manage.
// Node.js Lambda handler (zero dependencies beyond built-ins)
exports.handler = async (event) => {
const { url } = JSON.parse(event.body || "{}");
if (!url) return { statusCode: 400, body: "url required" };
const params = new URLSearchParams({
access_key: process.env.SNAPAPI_KEY,
url,
width: "1280",
full_page: "true",
});
const res = await fetch(`https://snapapi.pics/v1/screenshot?${params}`);
const data = await res.json();
return { statusCode: 200, body: JSON.stringify({ screenshot_url: data.url }) };
};
import json, os, urllib.request, urllib.parse
def handler(event, context):
body = json.loads(event.get("body", "{}"))
url = body.get("url")
if not url:
return {"statusCode": 400, "body": "url required"}
params = urllib.parse.urlencode({
"access_key": os.environ["SNAPAPI_KEY"],
"url": url,
"full_page": "true",
})
with urllib.request.urlopen(f"https://snapapi.pics/v1/screenshot?{params}") as r:
data = json.loads(r.read())
return {"statusCode": 200, "body": json.dumps({"screenshot_url": data["url"]})}
With SnapAPI, your Lambda function needs minimal memory — 128MB is usually sufficient since there is no browser process. Set a timeout of 30–60 seconds to account for SnapAPI's rendering time (typically 1–3 seconds) plus network overhead. This is much more reasonable than the 3–10 minute timeouts sometimes needed when running Puppeteer in Lambda for complex pages.
Combine SnapAPI with EventBridge rules for scheduled screenshot monitoring. An EventBridge rule fires your Lambda function every hour, the function calls SnapAPI, and the result gets stored in S3 or DynamoDB. This creates a simple, serverless visual monitoring system with zero infrastructure beyond the Lambda function and EventBridge rule.
// Scheduled monitoring Lambda
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const s3 = new S3Client({ region: "us-east-1" });
exports.handler = async () => {
const URLS = process.env.MONITOR_URLS.split(",");
for (const url of URLS) {
const params = new URLSearchParams({
access_key: process.env.SNAPAPI_KEY, url, width: "1440", full_page: "true",
});
const res = await fetch(`https://snapapi.pics/v1/screenshot?${params}`);
const { url: screenshotUrl } = await res.json();
console.log(`Captured ${url} => ${screenshotUrl}`);
// Store in DynamoDB for comparison, alert on visual diff
}
};
Lambda pricing depends on memory allocation and execution time. A Lambda function using Puppeteer needs 1024–2048MB of memory (Chromium is memory-hungry) and takes 5–15 seconds per screenshot (cold start + rendering). At 5,000 screenshots/month: approximately $2.50 in Lambda compute costs plus the EC2 costs if you moved to a container approach, plus $0 in headache-free operation (ha). With SnapAPI Starter at $19/month: Lambda functions use 128MB (fast, cheap) and run for 2–4 seconds each. Lambda compute cost drops to under $0.50/month. Total: $19.50/month — cheaper and infinitely more maintainable. Get started free today.
Puppeteer requires Chromium, which is a large binary around 170MB uncompressed. AWS Lambda has a 50MB zip limit and 250MB unzipped limit, so you are immediately forced into Lambda Layers, custom Docker images, or community solutions like chrome-aws-lambda. Even then, cold starts can hit 5 to 15 seconds. ARM64 Lambda (Graviton) is cheaper but requires ARM builds of Chromium. Maintenance is ongoing: every Lambda runtime update, Node version bump, or Puppeteer version change can break your layer. Teams typically spend 20 to 40 hours getting initial setup right, then face recurring maintenance every few months.
Instead of running a browser in your Lambda function, make a single HTTPS request to SnapAPI's screenshot endpoint. Your Lambda stays tiny with no layers, no Docker, and no Chromium binary. The screenshot infrastructure runs on SnapAPI's dedicated servers with warm browser pools, giving consistent performance without Lambda cold-start penalties.
// handler.mjs
export const handler = async (event) => {
const targetUrl = event.queryStringParameters?.url || 'https://example.com';
const params = new URLSearchParams({
access_key: process.env.SNAPAPI_KEY,
url: targetUrl,
format: 'png',
width: '1280',
height: '800',
full_page: 'false',
delay: '500',
});
const response = await fetch(
'https://api.snapapi.pics/v1/screenshot?' + params
);
if (!response.ok) {
return { statusCode: 502, body: 'Screenshot failed' };
}
const buffer = Buffer.from(await response.arrayBuffer());
return {
statusCode: 200,
headers: { 'Content-Type': 'image/png' },
body: buffer.toString('base64'),
isBase64Encoded: true,
};
};
This handler uses only Node.js built-ins. Zero npm packages. Zero layers. Cold start is under 200ms because there is no Chromium to initialize.
# serverless.yml
service: screenshot-service
provider:
name: aws
runtime: nodejs20.x
architecture: arm64
memorySize: 256
timeout: 30
environment:
SNAPAPI_KEY: ${ssm:/snapapi/key}
functions:
screenshot:
handler: handler.handler
events:
- http:
path: /screenshot
method: get
import json, os, base64, urllib.request, urllib.parse
def handler(event, context):
target = event.get('queryStringParameters', {}).get('url', 'https://example.com')
params = urllib.parse.urlencode({
'access_key': os.environ['SNAPAPI_KEY'],
'url': target,
'format': 'png',
'width': '1280',
'full_page': 'false',
})
url = 'https://api.snapapi.pics/v1/screenshot?' + params
with urllib.request.urlopen(url, timeout=28) as r:
img = r.read()
return {
'statusCode': 200,
'headers': {'Content-Type': 'image/png'},
'body': base64.b64encode(img).decode(),
'isBase64Encoded': True,
}
For most production use cases you will want to store the screenshot rather than streaming it on every request. After fetching from SnapAPI, upload directly to S3 using the AWS SDK client that ships natively with Lambda runtimes. Store the S3 key in DynamoDB or your database alongside the source URL and a timestamp, so you can serve cached screenshots for subsequent requests and only regenerate when the TTL expires.
With Puppeteer on Lambda you typically need 1024 to 3008MB of memory to avoid OOM errors during Chromium initialization. At 1024MB, Lambda costs roughly 0.0000000167 dollars per ms. A cold start plus render easily takes 8000ms, costing 0.013 dollars per request before the SnapAPI alternative. With SnapAPI your Lambda only needs 256MB and runs for under 2000ms total including the API round-trip, costing under 0.003 dollars in Lambda compute. Add SnapAPI's cost of approximately 0.004 dollars per screenshot on the paid plan and you are still ahead, with zero maintenance overhead.
One advantage of the API approach is that concurrency is handled on SnapAPI's side. With Puppeteer on Lambda, running more than a handful of concurrent browser instances risks memory exhaustion and Lambda timeouts. With SnapAPI, you simply fire concurrent requests and let the API handle queuing and browser pool management. Your Lambda functions remain stateless and scale independently from the screenshot workload.
Sign up at snapapi.pics for a free account with 200 screenshots per month and no credit card required. Grab your API key from the dashboard, set it as an SSM parameter or Lambda environment variable, and deploy the handler above. Your first screenshot is ready in minutes, not days, and you never have to touch a Chromium binary again.
Environment variable: SNAPAPI_KEY — set via AWS SSM Parameter Store, Secrets Manager, or Lambda environment variables in the console or via IaC. Memory: 128 to 256MB is sufficient. Timeout: set to 30 seconds to accommodate SnapAPI's maximum job duration. Architecture: arm64 (Graviton2) works perfectly — no Chromium binary means no architecture-specific build issues. IAM: no special permissions needed unless you are writing results to S3, in which case add s3:PutObject to your Lambda execution role for the target bucket.
Questions? The SnapAPI docs are at snapapi.pics and the API key is ready the moment you sign up.