ScreenshotNeo

BlogHow-to

How to capture a screenshot of a page with Playwright and upload it to S3

Capture a page with Playwright and upload the PNG to Amazon S3 using Node.js. Compare an in-memory Buffer workflow with saving a local file.

By the ScreenshotNeo team4 October 20269 min read

Direct answer: In Node.js, use Playwright’s page.screenshot() to capture the page as a PNG Buffer, then pass that buffer as Body to AWS SDK for JavaScript v3’s PutObjectCommand. This avoids writing a temporary file. Use fullPage: true for the full scrollable document; omit it for the current viewport.

This guide assumes a supported Node.js runtime with ES modules. The examples use Chromium, the AWS SDK v3, and credentials supplied through the standard AWS credential provider chain. Install the packages and browser first, then set the bucket and AWS credentials for your environment.

1. Install Playwright and the S3 client

npm init -y
npm install playwright @aws-sdk/client-s3
npx playwright install chromium

Set S3_BUCKET to an existing bucket name. Configure AWS credentials using the environment, an AWS profile, an instance or task role, or another supported provider. Give the identity only the permissions it needs, such as permission to write objects to the intended bucket and key prefix. Do not put long-lived credentials in source code.

2. Capture into memory and upload with PutObject

Save as capture-to-s3.mjs. Replace the target URL and choose a deliberate object key. This example uses a timestamp so each run writes a distinct key; a stable key will overwrite the existing object with that key.

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

const bucket = process.env.S3_BUCKET;
if (!bucket) {
  throw new Error('Set S3_BUCKET to an existing S3 bucket name');
}

const targetUrl = 'https://example.com';
const key = `screenshots/${Date.now()}.png`;
const s3 = new S3Client({});
const browser = await chromium.launch();

try {
  const page = await browser.newPage({
    viewport: { width: 1440, height: 900 },
  });

  await page.goto(targetUrl, { waitUntil: 'load', timeout: 30_000 });
  const screenshot = await page.screenshot({ fullPage: true });

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

  console.log(`Uploaded s3://${bucket}/${key}`);
} finally {
  await browser.close();
}

Run it with S3_BUCKET configured in your shell and AWS credentials available to the SDK. The browser is closed even if navigation, capture, or upload fails. A completed screenshot call only means the image was captured; the awaited S3 request is what reports whether storage succeeded.

page.screenshot() returns a Promise<Buffer> when no path is provided. PNG is the default format. The S3 request sets ContentType explicitly so consumers can identify the object as an image.

3. Choose viewport or full-page capture

Need Playwright option What it captures
Visible browser area Omit fullPage or set it to false The current viewport
Entire scrollable document fullPage: true The whole page, beyond the current viewport
Specific rectangle clip: { x, y, width, height } A selected region of the page
Local artifact as well as upload path: 'screenshot.png' Writes the screenshot to a local file

The viewport dimensions affect viewport screenshots and the rendered layout. For full-page captures, check especially long pages: the resulting image can be large, and a page’s layout or lazy-loaded content may depend on scrolling. If content appears only after scrolling or interaction, make the page ready for capture before taking the screenshot.

4. Save a local file before uploading

Choose this workflow when the process also needs a local artifact, for example for debugging or another local step. Playwright writes the image to the given path; read its bytes and use those as the S3 object body.

import { readFile } from 'node:fs/promises';
import { chromium } from 'playwright';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';

const bucket = process.env.S3_BUCKET;
if (!bucket) throw new Error('Set S3_BUCKET');

const path = 'screenshot.png';
const key = `screenshots/${Date.now()}.png`;
const browser = await chromium.launch();
const s3 = new S3Client({});

try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'load', timeout: 30_000 });
  await page.screenshot({ path, fullPage: true });

  const body = await readFile(path);
  await s3.send(new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    Body: body,
    ContentType: 'image/png',
  }));
  console.log(`Uploaded s3://${bucket}/${key}`);
} finally {
  await browser.close();
}

The file approach adds disk I/O and requires a writable path. If the file is only an intermediate, arrange cleanup after a successful upload, or use a temporary directory appropriate to your runtime. Keep the in-memory variant for the simplest handoff when no local copy is needed.

5. Control readiness and screenshot output

Navigation completing does not always mean the exact content you want is ready. Use a readiness condition that matches the page:

await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.locator('main article').waitFor({ state: 'visible', timeout: 10_000 });
await page.screenshot({ fullPage: true });

Use the selector that represents meaningful page content; do not wait on an element that is optional or absent on some pages. A fixed delay can help with a known animation or delayed update, but it adds latency and can still be too short or unnecessarily long. For pages with lazy content, scroll the relevant areas into view before capture and allow their content to load.

Playwright’s screenshot API also supports options such as clip, quality, scale, and masking. Consult the Page API reference for current details and version-specific behavior. For JPEG output, select the format and set a quality value; use a matching content type such as image/jpeg. Avoid setting PNG-only assumptions for another format.

6. cURL, Python, and Node.js alternatives

The title’s implementation uses Playwright in Node.js. cURL and Python do not call the Playwright Node API directly; they can invoke the Node script, or use a screenshot service that captures the page remotely.

Run the Node.js capture script with cURL unavailable

For a local Playwright capture, run the script with Node.js. cURL is an HTTP client and cannot directly launch this Playwright code. To upload an already-created local screenshot with AWS CLI instead, use:

aws s3 cp screenshot.png s3://YOUR_BUCKET/screenshots/screenshot.png --content-type image/png

Python with Playwright and boto3

If the calling application is Python, its equivalent uses the Python Playwright package and boto3. Install them and the Playwright browser, then use the returned screenshot bytes as the S3 body:

pip install playwright boto3
playwright install chromium
import asyncio
import os
import boto3
from playwright.async_api import async_playwright

async def main():
    bucket = os.environ['S3_BUCKET']
    s3 = boto3.client('s3')
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        try:
            page = await browser.new_page(viewport={"width": 1440, "height": 900})
            await page.goto('https://example.com', wait_until='load', timeout=30_000)
            image = await page.screenshot(full_page=True)
            s3.put_object(
                Bucket=bucket,
                Key='screenshots/example.png',
                Body=image,
                ContentType='image/png',
            )
        finally:
            await browser.close()

asyncio.run(main())

This Python example is an alternative implementation, not a translation of the Node.js SDK calls. Configure boto3 credentials through its standard provider chain and scope write access to the required bucket and prefix.

7. S3 keys, access, and larger objects

  • Key design: Pick a prefix and decide whether runs should overwrite one predictable key or create unique/versioned objects. A timestamp is convenient for demonstration, but production naming should match retention and lookup needs.
  • Access model: A successful upload does not make an object publicly readable. Keep objects private by default and grant access through the permissions and delivery mechanism your application requires. Avoid adding a public ACL casually.
  • Content metadata: Set ContentType to match the screenshot format. Add cache metadata only when the intended retrieval behavior is clear.
  • Large objects: Ordinary screenshots generally fit a single PutObject request. For large-object workflows, AWS documents multipart upload and the JavaScript v3 @aws-sdk/lib-storage helper. Check current AWS service limits and SDK guidance for your workload; do not assume a single-request upload is appropriate for very large files.

See the AWS JavaScript PutObject examples and the AWS multipart upload guide for their current patterns.

8. Troubleshooting

Symptom Likely cause Fix
AccessDenied during upload The active AWS identity lacks write permission for the bucket or key. Check which credentials the SDK resolved and grant the narrow required write permission for the destination.
Bucket or region error The bucket name is wrong or the client is using an unsuitable region configuration. Confirm the bucket exists and configure the S3 client for the bucket’s region when your environment requires it.
Navigation timeout The page is slow, unreachable, or keeps connections open. Check the URL and network access; choose an appropriate timeout and readiness condition. Do not treat a timeout as a successful capture.
Screenshot is blank or incomplete The page was captured before its content appeared, content is lazy-loaded, or automation is blocked. Wait for a meaningful selector, scroll lazy content into view, and inspect the page state and navigation errors.
Image is only viewport-sized fullPage was omitted or false. Set fullPage: true if the full scrollable document is required.
Object uploads but is not viewable by a browser The object is private, the retrieval identity lacks access, or metadata is unsuitable. Use the intended authorized retrieval path and set the correct content type. Uploading does not grant public access.
Browser fails to launch in a container Chromium or required runtime dependencies are missing, or the environment restricts browser launch. Install the Playwright browser for the deployment environment and follow its platform guidance; inspect the launch error before changing sandbox settings.
Memory use spikes on long pages A full-page image can be large, and both the buffer and upload request consume memory. Capture a smaller clip or viewport, reduce dimensions where appropriate, or use an upload workflow designed for larger objects.

9. Performance, reliability, and cost

The in-memory path avoids a file write and read, but it holds the screenshot bytes in process memory while uploading. Full-page images, large viewports, and concurrent browser pages increase memory use. Limit concurrency to what the runtime can support, close pages and browsers reliably, and set navigation and selector timeouts so a stuck page does not hold resources indefinitely.

For reliability, handle failures at each stage separately: navigation, screenshot capture, and S3 upload. Log the target, chosen key, and stage of failure without logging secrets. If retrying an upload, decide whether the key is stable (retry overwrites the same object) or unique (retry can create duplicates). An upload response should be awaited and errors surfaced to the caller.

Cost depends on your browser compute and S3 usage, including stored objects and requests; the actual amount depends on workload and account pricing. Set a retention policy appropriate to the use case and avoid capturing pages more often or at larger dimensions than needed. Review AWS pricing for your region and usage rather than relying on a generic estimate.

10. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF, so you do not need to install and operate a browser for this capture. Its API documentation covers request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo returns the capture; upload the resulting file to S3 separately if S3 storage is part of your workflow.

Create a free account for 1,000 screenshots a month, with no card required.

11. Frequently asked questions

Does a successful Playwright screenshot mean the S3 upload succeeded?

No. The screenshot is captured locally in the process. Await s3.send() and handle its errors to know whether the upload succeeded.

Can I make the uploaded screenshot public?

Access depends on bucket and object permissions. Choose an access policy intentionally; a successful PutObject does not make an object public.

Should I use a timestamp in the S3 key?

Use one only if each capture should create a distinct object. Use a stable key when the desired behavior is to replace a known latest screenshot.

Can I upload a JPEG instead of a PNG?

Yes. Set the Playwright screenshot format and quality as needed, then set ContentType to image/jpeg so metadata matches the bytes.

References