How to Capture Product Page Screenshots with Playwright and Save Them to S3
Capture a product page with Playwright, upload the screenshot Buffer to S3 using AWS SDK for JavaScript v3, and handle readiness, permissions, and failures.
Use Playwright to navigate to the product page, capture a PNG as an in-memory Buffer, then pass that Buffer to AWS SDK for JavaScript v3’s PutObjectCommand. This avoids writing a temporary image file. The Node.js process needs working AWS credentials and permission to write to the target bucket and key prefix.
1. Install dependencies and prepare AWS access
This guide uses Node.js with ES modules. Create a project and install Playwright and the S3 client:
mkdir product-screenshots
cd product-screenshots
npm init -y
npm pkg set type=module
npm install playwright @aws-sdk/client-s3
npx playwright install chromium
Set configuration in the environment where the script will run. For example, in a local shell:
export AWS_REGION=us-east-1
export SCREENSHOT_BUCKET=my-product-screenshots
export PRODUCT_URL=https://example.com/products/widget
export PRODUCT_ID=widget-123
The region and bucket are examples; use values for your AWS account. The SDK’s default credential provider chain resolves credentials from the Node.js environment. Prefer an established identity mechanism and grant only the permissions needed for this task. Do not place long-lived secrets in source code. AWS documents the [credential provider chain](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html) and recommends [least-privilege permissions](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html).
2. Capture a product page and upload it
Save this as capture-product.mjs. It waits for DOM content, then gives the application a chance to render a product-specific readiness signal. Replace [data-product-ready] with a selector that exists when the content you need is ready, or set PRODUCT_READY_SELECTOR to a suitable selector.
import { chromium } from 'playwright';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const productUrl = process.env.PRODUCT_URL;
const bucket = process.env.SCREENSHOT_BUCKET;
const productId = process.env.PRODUCT_ID;
const region = process.env.AWS_REGION;
const readySelector = process.env.PRODUCT_READY_SELECTOR ?? '[data-product-ready]';
if (!productUrl || !bucket || !productId || !region) {
throw new Error('Set PRODUCT_URL, SCREENSHOT_BUCKET, PRODUCT_ID, and AWS_REGION');
}
const safeProductId = productId.replace(/[^a-zA-Z0-9_-]/g, '-');
const captureId = new Date().toISOString().replace(/[:.]/g, '-');
const key = `products/${safeProductId}/${captureId}-1440x1000.png`;
const s3 = new S3Client({ region });
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
});
const response = await page.goto(productUrl, {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
if (!response) {
throw new Error(`Navigation returned no main-document response: ${productUrl}`);
}
if (!response.ok()) {
throw new Error(`Product page returned HTTP ${response.status()}: ${productUrl}`);
}
// Use a selector that means the product content needed for the screenshot is ready.
await page.locator(readySelector).waitFor({ state: 'visible', timeout: 20_000 });
const image = await page.screenshot({
type: 'png',
fullPage: true,
animations: 'disabled',
});
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: image,
ContentType: 'image/png',
}));
console.log(`Uploaded s3://${bucket}/${key} (${image.length} bytes)`);
} catch (error) {
console.error(`Capture or upload failed for ${productUrl} -> s3://${bucket}/${key}`);
console.error(error);
process.exitCode = 1;
} finally {
await browser.close();
await s3.destroy();
}
Run it with node capture-product.mjs. The readiness selector is site-specific. If the page has no reliable selector, wait for a meaningful application condition or a short, bounded delay after navigation; a fixed sleep alone is not a universal guarantee that images, prices, or client-rendered sections are ready.
Playwright’s page.screenshot() returns a Buffer in Node.js. AWS SDK v3’s S3 upload uses a client and PutObjectCommand with a bucket, key, and body. See the [Playwright screenshot API](https://playwright.dev/docs/api/class-page#page-screenshot), [Playwright screenshot guide](https://playwright.dev/docs/screenshots), and [AWS SDK v3 S3 upload example](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_s3_code_examples.html).
3. Choose the capture scope and output
| Need | Playwright option | Trade-off |
|---|---|---|
| Visible browser viewport | await page.screenshot() |
Smaller and predictable dimensions; content below the fold is omitted. |
| Entire scrollable page | await page.screenshot({ fullPage: true }) |
Includes below-the-fold content; very long pages can produce large images. |
| One product region | await page.locator('.product-gallery').screenshot() |
Useful for a gallery, price area, or specification panel; selector must identify the intended element. |
| JPEG or WebP | type: 'jpeg' or type: 'webp' |
Lossy formats can reduce size; use quality where supported and decide whether visual fidelity permits compression. |
For example, to capture the viewport as JPEG and upload it with matching metadata:
const image = await page.screenshot({ type: 'jpeg', quality: 85 });
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: `products/${safeProductId}/${captureId}.jpg`,
Body: image,
ContentType: 'image/jpeg',
}));
Playwright supports PNG, JPEG, and WebP screenshot output. Its options also let you choose CSS-pixel or device-pixel scale, disable animations, apply screenshot-specific styles, and mask selected locators. Use fullPage for page capture and locator screenshots for a specific element; consult the [screenshot option reference](https://playwright.dev/docs/api/class-page#page-screenshot) for current details.
4. Make captures repeatable and safe
- Wait for content, not just navigation.
domcontentloadedmeans the document was parsed, not that product data, images, or client rendering finished. Wait for the page’s real ready state. Network idle can help on some pages, but analytics and long-lived connections can make it unsuitable. - Set the viewport and device scale. Fixing viewport dimensions and
deviceScaleFactormakes the output dimensions more consistent. Use the same browser and execution environment for image comparisons. - Reduce animation noise. The sample disables animations. You can also provide screenshot-specific styles or mask dynamic regions when that suits the use case.
- Choose a key strategy deliberately. The timestamped key above preserves each capture and reduces accidental overwrites. For a latest-image workflow, use a stable key such as
products/widget/latest.png; concurrent jobs will overwrite it, so use versioned keys if you need history. - Protect displayed information. Product pages can vary by account, location, or personalization. Avoid capturing sensitive regions when appropriate, restrict bucket access, and set a retention policy that matches your collection purpose.
- Close resources on failure. The
finallyblock closes the browser and S3 client if navigation, capture, or upload fails.
Playwright notes that rendering can vary across operating systems, browser versions, settings, hardware, power sources, and headless mode. Pin and reuse the same environment when comparing screenshots; see its [visual comparisons guidance](https://playwright.dev/docs/test-snapshots).
5. Configure S3 access and understand CORS
The identity used by the script needs permission to write objects to the chosen bucket and key prefix. A narrowly scoped policy should permit the required s3:PutObject action on that location. The account or role may also need other permissions for other operations, but do not grant them without a requirement. Use AWS’s [IAM guidance](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) to scope access.
This example uploads from a Node.js server process using the AWS SDK. It does not need bucket CORS configuration for that server-side request. CORS applies to browser-origin requests. If you change the design so frontend JavaScript uploads directly to S3, configure CORS for the browser origin and separately authorize the operation with AWS permissions. CORS rules do not grant write authorization; see [Amazon S3 CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html).
For the ordinary product-page screenshot shown here, start with PutObjectCommand. If the workflow later uploads large artifacts and needs multipart upload behavior, AWS SDK v3 provides @aws-sdk/lib-storage; see the [AWS multipart upload guidance](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrate-s3.html).
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
Executable doesn't exist or browser launch fails |
Playwright’s browser binary is not installed in this environment. | Run npx playwright install chromium during setup. In restricted deployments, ensure the required browser dependencies are present. |
| Navigation timeout | The page is slow, blocked, or never reaches the selected navigation event. | Check that the URL is reachable from the runtime, choose an appropriate waitUntil condition, and set a bounded timeout. Then wait separately for the product content you need. |
| Screenshot is blank or missing product details | Capture ran before client-rendered content or images were ready, or the page showed an error/consent state. | Wait for a page-specific ready selector, inspect the response and page state, and ensure required images have loaded before capture. |
AccessDenied on S3 |
Credentials are absent, resolve to the wrong identity, or lack write permission for the bucket/key. | Check the resolved AWS identity and region, then grant only the required write permission to the exact destination prefix. Check bucket policies and any organization-level restrictions. |
| Bucket or region error | The configured bucket name or region does not match the bucket’s location. | Verify SCREENSHOT_BUCKET and set AWS_REGION for the target bucket. |
| Image downloads incorrectly or displays as a generic file | The object’s content type does not match the bytes or filename extension. | Keep screenshot type, object key extension, and ContentType aligned, such as PNG with image/png. |
| Output differs between runs | Dynamic content, animations, fonts, browser versions, or host rendering differ. | Wait for stable content, disable or mask changing regions where suitable, and run with a consistent browser and host environment. |
| Browser-origin upload gets a CORS error | A frontend request is not allowed by the bucket’s CORS rule, or authorization is missing. | Configure the required origin/method/headers in CORS and provide valid AWS authorization. CORS alone never authorizes the write. |
7. Performance, reliability, and cost
A full-page screenshot can consume more memory and produce a larger object than a viewport or element capture. If only the gallery or price panel is needed, capture that locator. Choose PNG when lossless detail matters; consider JPEG or WebP when smaller lossy output is acceptable. Avoid unnecessary temporary-file writes when the Buffer can go straight to S3.
Keep browser work and S3 upload in a failure-safe structure, log the URL and object key for diagnosis, and never log credentials. For batch jobs, bound concurrency to the capacity of the runtime and target site; excessive parallel browser sessions can exhaust memory or trigger site protections. Retry transient upload failures with a deliberate limit and avoid blindly repeating captures if they have side effects or if duplicate objects matter. Timestamped keys make retries easier to distinguish; stable keys make retries overwrite the same object.
AWS charges for S3 storage and requests according to the account’s region, storage class, and usage. The screenshot workflow itself does not imply a fixed cost: estimate expected object sizes, capture frequency, retention, and request volume using the current [Amazon S3 pricing page](https://aws.amazon.com/s3/pricing/). No universal cost or speed figure applies to every page and deployment.
Or skip the browser setup
If you need a screenshot without maintaining a browser runtime, ScreenshotNeo provides a website screenshot API and MCP server. Its one-call API returns an image or PDF. See the ScreenshotNeo API documentation for request options and formats.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. The MCP server gives AI agents tools to take screenshots, inspect page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to get started.
FAQ
Can I upload the screenshot without saving it locally first?
Yes. In Node.js, Playwright returns a Buffer from page.screenshot(), and the AWS SDK accepts that Buffer as the Body for PutObjectCommand.
Does a server-side SDK upload require S3 CORS?
No. CORS is relevant to browser-origin requests. A Node.js process making the SDK call is authorized through AWS credentials and permissions.
Should I use full-page or viewport capture for a product page?
Use viewport capture for a consistent above-the-fold image, full-page for the entire listing, and a locator screenshot when one component is the deliverable.
Why can the same page produce different screenshots?
The page may contain changing data or animation, and rendering can vary with the browser and host environment. Keep those inputs consistent when repeatability matters.


