Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Use a Lambda-compatible Chromium build with puppeteer-core or Playwright. In a Node.js handler, validate the target URL, launch Chromium with the build’s executable path and arguments, navigate with an explicit readiness condition and timeout, then capture image bytes. Return a small image directly or upload it to Amazon S3 when the output may exceed Lambda’s synchronous response limit. The hard part is packaging and maintaining a compatible browser—not calling the screenshot method.

What do you need to take a screenshot in Lambda?

A local Puppeteer setup usually has a browser available because the full Puppeteer package downloads one. puppeteer-core does not; in Lambda, you must supply a Chromium build that works in the deployment environment and keep its version compatible with the Puppeteer package. A commonly used pairing is puppeteer-core with @sparticuz/chromium. Match the versions using the selected packages’ current documentation rather than assuming any combination will work.

The flow is: accept a URL, reject unsupported protocols, start Chromium, open a page, navigate, capture bytes, and return or store the result. AWS’s browser-automation example uses Lambda with Puppeteer and S3; the same architecture can support a URL-to-image function.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Install the application dependencies

For a ZIP deployment, install the packages in your Node.js project and include them with the handler. The following command installs the packages used in the example; choose and lock mutually compatible versions for your deployment.

npm install puppeteer-core @sparticuz/chromium

Keep the application code, Chromium package, Puppeteer package, and Lambda runtime aligned. A browser that launches locally may still fail in Lambda because of differences in binary path, operating-system libraries, CPU architecture, or package size.

How do you capture and return a screenshot from a Node.js Lambda handler?

This handler reads a URL from either an API Gateway query parameter or a direct Lambda event, restricts the protocol to HTTP or HTTPS, launches Chromium once per warm execution environment, and returns a PNG as a base64-encoded API response.

import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';

let browserPromise;

async function getBrowser() {
  if (!browserPromise) {
    browserPromise = (async () => {
      const executablePath = await chromium.executablePath();
      return puppeteer.launch({
        args: chromium.args,
        executablePath,
        headless: chromium.headless,
      });
    })();
  }
  return browserPromise;
}

export const handler = async (event) => {
  let url;
  try {
    url = new URL(event?.queryStringParameters?.url || event?.url);
  } catch {
    return {
      statusCode: 400,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Provide a valid URL.' }),
    };
  }

  if (!['http:', 'https:'].includes(url.protocol)) {
    return {
      statusCode: 400,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Only HTTP(S) URLs are allowed.' }),
    };
  }

  let page;
  try {
    const browser = await getBrowser();
    page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });

    const response = await page.goto(url.href, {
      waitUntil: 'networkidle2',
      timeout: 30000,
    });

    const png = await page.screenshot({ type: 'png' });
    return {
      statusCode: 200,
      headers: {
        'content-type': 'image/png',
        'cache-control': 'no-store',
        'x-page-status': String(response?.status() ?? 'unknown'),
      },
      isBase64Encoded: true,
      body: Buffer.from(png).toString('base64'),
    };
  } catch (error) {
    console.error('Screenshot capture failed', {
      hostname: url.hostname,
      message: error instanceof Error ? error.message : String(error),
    });
    return {
      statusCode: 502,
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ error: 'Could not capture the requested page.' }),
    };
  } finally {
    if (page) await page.close().catch(() => {});
  }
};

Use an ES-module project configuration, such as "type": "module" in package.json, if deploying this exact import-based handler. Configure the function’s handler entry point to the file and exported name you use, for example index.handler. The function must have enough execution time for Chromium startup and page rendering; the navigation timeout in the example is 30 seconds, but the Lambda timeout must allow for the whole invocation, not only navigation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What the handler does—and does not—treat as an error

The URL parser rejects missing or malformed input, and the protocol check blocks schemes such as file:. A successful navigation does not necessarily mean the site returned HTTP 200: Puppeteer can complete navigation for HTTP error responses such as 404 or 500. The example exposes the response status in a header so the caller can decide whether to keep the image. If your API contract requires only successful pages, inspect response?.ok() and return an error status when it is false.

The protocol check is not a complete defense against server-side request forgery (SSRF). If a caller can submit arbitrary URLs, enforce an approved-host allowlist or robust network-level egress rules that block loopback, private, link-local, metadata-service, and internal DNS targets. DNS rebinding and redirects also need consideration: validate destinations after resolution and prevent navigation from reaching prohibited networks. Do not expose an unrestricted public screenshot endpoint without authentication, rate limits, abuse controls, and cost protection.

How do you save a Lambda screenshot to S3?

Use S3 when the image, associated metadata, or next processing step may exceed Lambda’s synchronous payload allowance. AWS documents a 6 MB limit for synchronous request and response payloads. S3 also gives the result a durable location; Lambda’s /tmp directory is temporary execution storage, not permanent result storage.

Install the AWS SDK S3 client if it is not already available in your deployment:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
npm install @aws-sdk/client-s3

After producing png in the handler above, upload the buffer instead of returning it. Set the bucket name in the function environment as SCREENSHOT_BUCKET, and grant the Lambda execution role only the S3 permissions required for the target bucket and key prefix.

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

const s3 = new S3Client({});

// After: const png = await page.screenshot({ type: 'png' });
const bucket = process.env.SCREENSHOT_BUCKET;
if (!bucket) throw new Error('SCREENSHOT_BUCKET is not configured');

const key = `captures/${crypto.randomUUID()}.png`;
await s3.send(new PutObjectCommand({
  Bucket: bucket,
  Key: key,
  Body: Buffer.from(png),
  ContentType: 'image/png',
}));

return {
  statusCode: 200,
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ bucket, key }),
};

In a complete file, place the imports and S3Client initialization at module scope, and replace the direct-image return in the earlier handler with the upload-and-JSON response. Configure encryption and object retention to fit your application. Avoid returning an object URL as if it were automatically public; use an authorized retrieval path or a deliberately configured signed URL.

How should you package Chromium for Lambda?

Choose a deployment format based on package size, browser maintenance, cold-start behavior, and your team’s operational preferences. AWS’s documented maximum for a ZIP deployment package, including layers, is 250 MB unzipped.

Approach When it fits Trade-offs to plan for
ZIP plus layer You want application code separate from the browser binary. The combined unzipped function package and layers must fit within Lambda’s 250 MB limit. Verify the actual packaged size and architecture.
Container image Browser dependencies make a ZIP deployment awkward, or you want to package the runtime and browser together. Build and maintain the image, keep Chromium compatible with Puppeteer, and account for image size and deployment workflow. AWS documents Lambda container-image deployment.
Playwright with a Lambda layer Your application uses Playwright or you prefer its API. The layer must provide compatible playwright-core and Chromium. A Lambda layer example returns a base64-encoded PNG; browser packaging and compatibility remain your responsibility.
Remote screenshot API You want a smaller Lambda handler without bundling Chromium. This adds an external service dependency, credentials, network latency, data-transfer considerations, and a third party that receives the target URL or page request.

There is no universal cold-start figure to apply: a published example tested in a Lambda emulator is not a measurement of a deployed AWS function. Measure your own cold and warm invocations with the selected architecture, memory, package format, and target pages. Likewise, test on the CPU architecture configured for the function; a binary or layer built for another architecture may not launch.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do you control when the page is ready to capture?

The screenshot is only as complete as the page state at capture time. Puppeteer’s default navigation timeout is 30 seconds, and waitUntil selects the browser lifecycle event that allows navigation to finish.

Choose a navigation condition deliberately

  • networkidle2 waits for a quiet network period and can help when client-side assets load after the initial document. It can also take too long or fail to settle on pages with long-lived connections, polling, or continuously loading resources.
  • For pages that do not need all network activity to stop, use a less restrictive navigation event and then wait for an app-specific selector or readiness signal.
  • For dynamic interfaces, wait for the element that proves the content you need has rendered before calling page.screenshot(). A fixed delay can work for a known site, but it is less reliable than a meaningful selector.
await page.goto(url.href, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('[data-capture-ready="true"]', { timeout: 10000 });
const png = await page.screenshot({ type: 'png' });

Replace the selector with one present on the target application. If the page has no reliable ready marker, use a bounded delay and handle timeout failures rather than allowing an invocation to run indefinitely.

Set rendering dimensions and output format

Set the viewport before navigation when responsive layout matters, and use a consistent device scale factor when comparing captures. Puppeteer supports full-page capture, element capture, clipping, and JPEG quality options; select the smallest output that meets the downstream need.

// Entire scrollable page
const fullPagePng = await page.screenshot({ type: 'png', fullPage: true });

// JPEG can reduce output size; quality is relevant for JPEG
const jpeg = await page.screenshot({ type: 'jpeg', quality: 80 });

// Capture one element
const chart = await page.$('#chart');
if (!chart) throw new Error('Chart element was not found');
const chartPng = await chart.screenshot({ type: 'png' });

Large full-page screenshots can consume more memory and create a response too large for synchronous invocation. Fonts, images, lazy-loaded content, animations, and application-specific rendering can change the result. If visual consistency matters, wait for the required assets or application state and test representative pages.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do you keep the function reliable and secure?

  • Set layered timeouts. Bound navigation and selector waits, and set the Lambda timeout high enough to include browser startup, rendering, screenshot encoding, and any S3 upload.
  • Close each page. The example closes the page in finally, including on navigation and screenshot errors. Reusing a warm browser can reduce repeated launches, but account for concurrent invocations and stale browser processes in your reuse strategy.
  • Keep logs useful and safe. Record elapsed time, status, and output bytes to diagnose slow or failed captures. Avoid logging credentials, authorization headers, cookies, or sensitive query parameters in submitted URLs.
  • Use least privilege. Limit the execution role’s S3 writes to the intended bucket and key prefix. Apply encryption and access controls appropriate to captured page contents.
  • Protect the URL fetcher. Authenticate callers, apply rate limits and quotas, defend against SSRF, and restrict outbound access where possible. A screenshot endpoint can otherwise be abused to access internal services or generate unexpected browser and transfer costs.
  • Plan concurrency and memory. Chromium, page rendering, and image encoding use memory and CPU. Test under expected concurrency and with large or slow pages; do not infer capacity from a local run.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Why does Puppeteer work locally but fail in Lambda?

Most failures come from packaging or environment mismatch rather than the screenshot call itself. Check the error and deployment configuration in this order.

Symptom Likely cause What to check or change
Executable missing or launch fails puppeteer-core has no bundled browser, or the Chromium executable path is wrong. Use a Lambda-compatible Chromium package, pass its resolved executable path and supplied launch arguments, and verify the package is included in the deployment.
Browser fails after deployment Browser and Puppeteer versions, CPU architecture, or runtime assumptions do not match. Use a documented compatible package pairing, build for the function’s architecture, and test the deployed artifact rather than only the local project.
Deployment package is rejected The ZIP and layers exceed Lambda’s unzipped package limit. Measure the combined unzipped size; consider a container image if browser dependencies make the ZIP impractical.
Navigation timeout The target keeps connections open, loads slowly, or never reaches the selected lifecycle state. Choose a suitable waitUntil, wait for a relevant selector, and use bounded timeouts that fit the function’s total execution budget.
Image is blank or incomplete The screenshot runs before client rendering, lazy images, fonts, or other required assets are ready. Wait for an application-ready selector or other reliable signal; check viewport, network access, and whether the content is below the fold.
Caller receives an error for a seemingly valid page The site returned an HTTP error status, or the invocation exceeded its response or time limit. Inspect the navigation response status and logs. Store large captures in S3 instead of returning their bytes synchronously.
Invocation runs out of memory or is slow under load Large full-page images, concurrent browser pages, or constrained function resources. Test representative page sizes and expected concurrency; reduce output dimensions or capture a specific element where appropriate.

Or skip the browser setup

If you do not want to ship Chromium in Lambda, ScreenshotNeo accepts a URL in a single request and returns an image or PDF. Its clean-shot flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response includes X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents.

For a Node.js Lambda handler, make the request and pass through the resulting image bytes or store them in S3. Keep the API key in a secret or protected environment variable, not in source code.

const q = new URLSearchParams({ access_key: process.env.SCREENSHOTNEO_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}`);
const image = Buffer.from(await res.arrayBuffer());

See the ScreenshotNeo API documentation for request options. It includes full-page and element capture, device and viewport settings, dark mode, custom CSS and JavaScript, wait conditions, request blocking, cookies and headers, caching, signed links, asynchronous jobs, bulk capture, and PDF settings. ScreenshotNeo is available at screenshotneo.com. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000, and yearly billing gives two months free. Sign up for the free plan to try it without a card.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can a Lambda screenshot function capture a page that requires login?

It can if you provide the required session state, such as authorized cookies or headers, and the site permits that access. Treat credentials as secrets, avoid writing them to logs, and confirm you have permission to capture the page.

Can I trigger a screenshot without waiting for the image in the HTTP response?

Yes. A common design is to accept a job, capture asynchronously, and write the result and job status to S3 or another application store. That avoids holding an HTTP client open during slow page rendering.

Why is text or an image missing even though navigation completed?

Navigation completion is not the same as application readiness: client-side code, web fonts, lazy images, and animations may still be in progress. Wait for the specific content your capture requires, and make sure the viewport exposes any lazy-loaded content.

Can I use a Lambda function URL for a public screenshot endpoint?

A function URL can expose the handler over HTTP, but public reachability does not make arbitrary URL fetching safe. Require suitable authentication or access controls, defend against SSRF, and impose rate and cost limits before accepting caller-supplied targets.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.