ScreenshotNeo

BlogHow-to

How to Upload Puppeteer Screenshots from AWS Lambda to S3

Capture a webpage with Puppeteer in AWS Lambda, upload the screenshot bytes to S3, configure permissions, and fix common deployment errors.

By the ScreenshotNeo team1 October 20268 min read

Direct answer: launch a Lambda-compatible Chromium build with Puppeteer, navigate to the page, call page.screenshot() to obtain image bytes, and pass those bytes to an AWS SDK for JavaScript v3 PutObjectCommand. The Lambda execution role needs s3:PutObject permission for the destination bucket and key prefix. Keep the screenshot in memory unless your browser package or workload needs files; use Lambda’s writable /tmp directory for browser profiles, caches, or staged images.

End-to-end flow

  1. Choose a Puppeteer and Chromium package that matches the Lambda Node.js runtime and CPU architecture.
  2. Give the function role permission to write only to the required S3 object path.
  3. Launch Chromium with writable profile and cache paths under /tmp when required.
  4. Navigate with a wait condition appropriate for the target site.
  5. Capture PNG, JPEG, or WebP bytes with page.screenshot().
  6. Await PutObjectCommand before returning success, then close the browser in a finally block.

Puppeteer documents both byte output and base64 output for screenshots. AWS documents the JavaScript v3 S3Client/PutObjectCommand pattern. See the Puppeteer screenshot API and the Amazon S3 PutObject API.

Prepare the Lambda deployment

Pin a compatible browser set

Standard Puppeteer installation downloads a browser intended for that Puppeteer release. Package-manager settings that disable install scripts can leave the deployed artifact without a browser. For Lambda, treat these as one compatibility set:

  • Lambda Node.js runtime version
  • Linux operating system and CPU architecture (for example, arm64 or x86_64)
  • Chromium or Chrome build
  • puppeteer or puppeteer-core version
  • Packaging method: bundled browser, layer, container image, or another compatible artifact

The general Puppeteer installation guide does not guarantee that a particular Lambda layer or package works with every runtime. Pin versions, build for the same architecture as the function, and verify that the deployed artifact contains the executable.

Install the AWS SDK client

npm install @aws-sdk/client-s3 puppeteer-core

The exact Chromium package is runtime-specific. Keep it separate from the S3 code so you can replace the browser artifact without changing the upload path.

Configure writable paths

Lambda provides temporary storage at /tmp. Its configured size ranges from 512 MB to 10,240 MB in 1 MB increments. The directory is temporary, unique to an execution environment, and not durable storage. Browser profiles and caches must use writable locations; Puppeteer’s troubleshooting guidance recommends locations under /tmp for constrained environments.

IAM policy for the Lambda execution role

s3:PutObject is required to add an object. Scope the resource to the bucket and prefix that this function actually writes:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::YOUR_BUCKET/screenshots/*"
    }
  ]
}

Replace YOUR_BUCKET and the prefix. A bucket policy, organization policy, or permissions boundary can still deny the request, so inspect those policies when an upload returns AccessDenied.

When more permissions are needed

  • Add s3:PutObjectAcl only if the request sets an object ACL.
  • Add s3:PutObjectTagging only if the request writes object tags.
  • For SSE-KMS, AWS documents additional kms:GenerateDataKey and kms:Decrypt permissions, plus authorization in the KMS key policy.

Do not make screenshots public by default. Bucket-owner-enforced Object Ownership disables ACL-based public access; choose a deliberate private bucket and delivery design.

Complete Node.js Lambda example

This handler keeps the screenshot in memory and uploads it directly. Replace the browser launch options with those required by your selected Lambda-compatible Chromium build.

import puppeteer from 'puppeteer-core';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

export const handler = async (event = {}) => {
  const bucket = process.env.SCREENSHOT_BUCKET;
  if (!bucket) throw new Error('SCREENSHOT_BUCKET is not configured');

  const targetUrl = event.url || 'https://example.com';
  const safeId = String(event.id || Date.now()).replace(/[^a-zA-Z0-9._-]/g, '-');
  const key = `screenshots/${safeId}.png`;
  let browser;

  try {
    browser = await puppeteer.launch({
      // Supply executablePath or the args required by your Chromium package.
      // Example flags vary by package and runtime.
      args: ['--no-sandbox', '--disable-setuid-sandbox'],
      headless: true,
      userDataDir: '/tmp/puppeteer-profile'
    });

    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
    await page.goto(targetUrl, {
      waitUntil: 'networkidle0',
      timeout: 45000
    });

    const image = await page.screenshot({
      type: 'png',
      fullPage: true
    });

    await s3.send(new PutObjectCommand({
      Bucket: bucket,
      Key: key,
      Body: image,
      ContentType: 'image/png'
    }));

    return {
      statusCode: 200,
      body: JSON.stringify({ bucket, key, url: targetUrl })
    };
  } finally {
    if (browser) await browser.close();
  }
};

Set SCREENSHOT_BUCKET as an environment variable. The function returns only after S3 acknowledges the complete object upload. PutObject replaces the complete object value for an existing key; it is not a partial metadata update.

Capture options that affect the uploaded object

Need Puppeteer option Operational note
Entire document fullPage: true Long pages can consume more browser memory and produce larger objects.
One viewport Omit fullPage Use setViewport for deterministic dimensions.
JPEG type: 'jpeg', quality: 80 Set ContentType: 'image/jpeg'.
WebP type: 'webp', quality: 80 Set ContentType: 'image/webp' and confirm browser support.
Transparent PNG Page styling and PNG output Ensure the page background is transparent before capture.

Choose waitUntil according to the site. networkidle0 can wait indefinitely on pages with analytics or long polling; use domcontentloaded followed by an explicit selector wait when that better matches the page.

Memory upload versus /tmp staging

Approach Use it when Trade-off
In-memory bytes The screenshot and browser fit comfortably within the function memory. Simple and avoids an extra file operation; large full-page images increase memory pressure.
Stage in /tmp Your Chromium package needs files, or downstream processing expects a path. Uses ephemeral storage and requires cleanup; files disappear when the environment is recycled.
import { writeFile } from 'node:fs/promises';

const image = await page.screenshot({ type: 'png' });
const path = '/tmp/capture.png';
await writeFile(path, image);

await s3.send(new PutObjectCommand({
  Bucket: bucket,
  Key: key,
  Body: image, // or a readable stream created from the file
  ContentType: 'image/png'
}));

Size ephemeral storage for browser extraction, profile and cache files, and staged artifacts. Never use /tmp as the durable copy of the screenshot.

Testing the S3 result

After the Lambda call, verify the exact bucket, region, and key. A successful PutObject response means S3 accepted the complete object.

aws s3api head-object \\
  --bucket YOUR_BUCKET \\
  --key screenshots/example.png

For a private bucket, use an authenticated AWS client or a presigned URL for delivery. Do not add a public ACL merely to test the object.

Python and cURL equivalents

The capture code above is Node.js because Puppeteer is a Node library. These examples show how another service can upload already-created screenshot bytes.

Python with boto3

import boto3

s3 = boto3.client("s3")
with open("capture.png", "rb") as image:
    s3.put_object(
        Bucket="YOUR_BUCKET",
        Key="screenshots/capture.png",
        Body=image,
        ContentType="image/png",
    )

cURL with a presigned PUT URL

curl --fail --upload-file capture.png \\
  -H 'Content-Type: image/png' \\
  'PRESIGNED_PUT_URL'

A normal S3 PutObject request must be authenticated. Use the AWS CLI/SDK or generate a presigned URL rather than placing long-lived credentials in a cURL command.

Common errors and fixes

Error Likely cause Fix
Browser executable not found Install scripts were disabled, or the deployed artifact lacks Chromium. Inspect the build output, enable the required browser install step, and verify the executable path in the Lambda artifact.
Chromium launch fails while writing files Profile or cache points to a read-only directory. Set user-data, cache, and temporary paths under /tmp; increase ephemeral storage if extraction or caches need more room.
AccessDenied from S3 Role, bucket policy, object ARN, prefix, region, or KMS policy blocks the write. Check the execution role’s s3:PutObject resource and every applicable bucket and KMS policy.
Function reports success but object is absent The SDK promise was not awaited, or the wrong bucket/key/region was inspected. await s3.send(...), log the resolved bucket and key, and run head-object against those exact values.
Navigation timeout The page never reaches the selected lifecycle condition, or outbound access is restricted. Choose a suitable wait strategy, set a bounded timeout, wait for a specific selector, and verify the function’s network path.
Out-of-memory or Lambda timeout Large pages, high concurrency, browser startup, or oversized screenshots exceed configured resources. Measure the real target page, reduce viewport or full-page dimensions where possible, raise memory and timeout, and reuse no browser state across requests unless it is deliberately managed.
Intermittent failures on repeated invocations Stale temporary files, browser processes, or assumptions about execution-environment reuse. Use unique keys, clean up in finally, tolerate a fresh /tmp, and do not depend on previous invocation files.

Performance, reliability, and cost considerations

  • Browser startup: launching Chromium is often a major part of invocation time. Keep the package small and select a compatible deployment artifact; do not assume a local browser bundle transfers unchanged.
  • Page behavior: full-page captures and pages with many images or scripts require more memory and time. Use explicit waits that represent the content you need.
  • S3 writes: upload the bytes once and await completion. If you need resizing, tagging, or alternate destinations, perform those steps intentionally rather than treating PutObject as a metadata-only operation.
  • Retries: retries can create duplicate objects when keys are timestamped. If idempotency matters, derive the key from a request identifier and decide whether overwriting is acceptable.
  • Storage: S3 storage and request charges depend on your AWS account, region, volume, and retention policy. Set lifecycle rules for old screenshots when they are no longer needed.
  • Lambda billing: memory size and execution duration affect Lambda cost. Profile the target pages in the deployed runtime before choosing settings; no universal benchmark applies to every page or browser package.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API when you do not want to package and operate Chromium in Lambda. One GET request returns a PNG, JPEG, WebP, or PDF. Cookie and consent banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server supports AI-agent tools including take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There are 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can I upload the screenshot without writing a file?

Yes. Puppeteer’s default screenshot result is byte data, and the bytes can be passed directly as the S3 Body.

Does Lambda need public internet access?

The function must be able to reach the target page and S3 endpoint. A VPC configuration can change outbound connectivity, so verify the network path for your deployment.

Should each screenshot use a unique S3 key?

Use unique keys when retaining every capture. Use deterministic keys when intentional overwriting provides idempotency.

Is /tmp shared between invocations?

An execution environment may be reused, but reuse is not guaranteed. Treat files as temporary and clean up anything your workflow does not need.

With Puppeteer, handle the banner in page automation before capture. ScreenshotNeo removes supported consent platforms and other listed widgets before the shot.