ScreenshotNeo

BlogHow-to

Create website screenshots with Python Selenium on an AWS Lambda function

Build a Python Lambda container that opens a website with Selenium, waits for a useful readiness signal, captures a PNG, and returns or stores it reliably.

By the ScreenshotNeo team4 October 202611 min read

To create website screenshots with Python Selenium on AWS Lambda, package Python, a compatible Chrome or Chromium browser, and its matching WebDriver in a Linux Lambda container image. At invocation time, validate the URL, start the browser with headless and writable temporary-profile settings, navigate, wait for a page-specific readiness condition, capture a PNG, send it to a deliberate destination, and always close the browser. A Selenium window screenshot captures the current browser window; it does not automatically mean a full-page capture.

This guide gives you a container-based starting point and a complete handler. Browser binaries and drivers must match the image’s operating system and architecture. The research sources support the AWS and Selenium workflow, but do not establish a tested Chrome/driver bundle; build and validate the exact image you intend to deploy.

1. Choose how the function returns the screenshot

Decide on the output path before implementing the handler:

Output path Good fit Considerations
Return Base64 in the Lambda response Small, synchronous requests where the caller needs the image immediately Base64 increases the response size. Keep the image and response within the invocation interface’s limits; use object storage for larger images.
Write to object storage and return a key Durable output, larger files, or asynchronous workflows Grant the function only the permissions it needs, and define retention and access policy.
Write to /tmp only Intermediate processing inside one invocation /tmp is local temporary storage, not a durable destination for callers.

The sample below returns Base64 so it has no extra storage dependency. It writes a temporary PNG first, checks Selenium’s save result, reads the bytes, and deletes the file. For production use with larger captures, adapt the marked output section to upload to your chosen storage service.

2. Package Python, the browser, and WebDriver

A Lambda container image is a practical option when the browser and native libraries make packaging awkward. AWS documents three routes: an AWS Python base image, an AWS OS-only image, or another base image. The AWS Python base image includes the runtime and Lambda components; an OS-only or other base needs a compatible Python runtime interface client. This example uses an AWS Python base image.

Lambda container images must be Linux-based and target one architecture. AWS sets a maximum uncompressed image size of 10 GB, including layers. Python 3.12 and later base images use Amazon Linux 2023 and microdnf (also available as dnf); older runtimes may use Amazon Linux 2 and yum. Check the current runtime tags and support dates before choosing a base image. AWS Python container image guide · Lambda container image requirements.

Keep the browser and driver installation specific to the exact base image and target architecture. Pin compatible browser and driver releases, place their binaries at known paths, and install the shared libraries the browser needs. Do not assume a package name or third-party browser bundle works for every Lambda image.

Project files

lambda-screenshot/
├── Dockerfile
├── handler.py
└── requirements.txt

requirements.txt:

selenium==4.XX.Y

Replace 4.XX.Y with a real Selenium version you have selected and pinned for your build. This placeholder is deliberate: select a release compatible with your chosen Python runtime and validate it with the browser and driver in the image.

Dockerfile (browser installation is intentionally represented as a build-specific step):

FROM public.ecr.aws/lambda/python:3.12

COPY requirements.txt ${LAMBDA_TASK_ROOT}/requirements.txt
RUN pip install --no-cache-dir -r ${LAMBDA_TASK_ROOT}/requirements.txt

# Install or copy a Chrome/Chromium binary and compatible WebDriver here.
# Add the browser's required shared libraries for this base image.
# Ensure both binaries are executable and record their paths.

COPY handler.py ${LAMBDA_TASK_ROOT}/handler.py
CMD ["handler.lambda_handler"]

This Dockerfile is a structural template, not a ready-to-deploy browser image: it will not launch a browser until you supply the browser, matching driver, and required shared libraries. Keeping that install step explicit avoids implying an unverified binary source or version. AWS’s guide explains the base image, task root, handler command, build, local invocation, and ECR flow: Deploy Python Lambda functions with container images.

3. Add the Selenium handler

The handler accepts a target URL, optional viewport dimensions and readiness selector, then returns the PNG as Base64. In an internet-facing function, allow only destinations your application is meant to capture; accepting arbitrary URLs creates security and resource-use risks. The example validates basic URL structure, but it is not a complete destination policy.

import base64
import json
import os
import tempfile
from urllib.parse import urlparse

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait

CHROME_BINARY = os.environ.get("CHROME_BINARY", "/opt/chrome/chrome")
CHROMEDRIVER_BINARY = os.environ.get("CHROMEDRIVER_BINARY", "/opt/chromedriver")
MAX_WAIT_SECONDS = int(os.environ.get("MAX_WAIT_SECONDS", "25"))


def validate_url(value):
    if not isinstance(value, str) or len(value) > 2048:
        raise ValueError("url must be a string no longer than 2048 characters")
    parsed = urlparse(value)
    if parsed.scheme not in ("http", "https") or not parsed.hostname:
        raise ValueError("url must be an absolute http or https URL")
    return value


def make_driver(width, height, page_timeout):
    options = Options()
    options.binary_location = CHROME_BINARY
    options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-gpu")
    options.add_argument(f"--window-size={width},{height}")
    options.add_argument("--user-data-dir=/tmp/chrome-profile")
    options.add_argument("--data-path=/tmp/chrome-data")
    options.add_argument("--disk-cache-dir=/tmp/chrome-cache")
    options.add_argument("--crash-dumps-dir=/tmp/chrome-crashes")

    service = Service(executable_path=CHROMEDRIVER_BINARY)
    driver = webdriver.Chrome(service=service, options=options)
    driver.set_page_load_timeout(page_timeout)
    driver.set_script_timeout(page_timeout)
    return driver


def lambda_handler(event, context):
    event = event or {}
    try:
        url = validate_url(event.get("url"))
        width = int(event.get("width", 1280))
        height = int(event.get("height", 800))
        if not (320 <= width <= 3840 and 240 <= height <= 3840):
            raise ValueError("width or height is outside the supported sample range")
        selector = event.get("ready_selector")
        if selector is not None and (not isinstance(selector, str) or len(selector) > 500):
            raise ValueError("ready_selector must be a CSS selector string")

        remaining_ms = context.get_remaining_time_in_millis() if context else 60_000
        # Leave time for screenshot encoding, response creation, and browser cleanup.
        page_timeout = max(2, min(35, remaining_ms // 1000 - 8))
        driver = None
        try:
            driver = make_driver(width, height, page_timeout)
            driver.get(url)
            if selector:
                WebDriverWait(driver, min(MAX_WAIT_SECONDS, page_timeout)).until(
                    lambda d: d.find_element("css selector", selector).is_displayed()
                )
            else:
                # This checks document readiness, not whether every visual asset is loaded.
                WebDriverWait(driver, min(MAX_WAIT_SECONDS, page_timeout)).until(
                    lambda d: d.execute_script("return document.readyState") == "complete"
                )

            with tempfile.NamedTemporaryFile(suffix=".png", dir="/tmp", delete=False) as image_file:
                image_path = image_file.name
            try:
                if not driver.save_screenshot(image_path):
                    raise RuntimeError("WebDriver could not save the screenshot")
                with open(image_path, "rb") as saved:
                    image_bytes = saved.read()
            finally:
                try:
                    os.unlink(image_path)
                except FileNotFoundError:
                    pass

            return {
                "statusCode": 200,
                "headers": {"content-type": "application/json"},
                "body": json.dumps({
                    "content_type": "image/png",
                    "encoding": "base64",
                    "image": base64.b64encode(image_bytes).decode("ascii"),
                    "width": width,
                    "height": height,
                }),
            }
        finally:
            if driver is not None:
                driver.quit()
    except ValueError as exc:
        return {"statusCode": 400, "body": json.dumps({"error": str(exc)})}
    except Exception as exc:
        # Log details to the function's configured logs; return a concise client error.
        print(json.dumps({"error": type(exc).__name__, "message": str(exc)}))
        return {"statusCode": 502, "body": json.dumps({"error": "Screenshot capture failed"})}

The sample’s default readiness check waits for document.readyState == complete, which does not prove that a single-page application’s data, lazy images, fonts, or animations are finished. Prefer a selector tied to the content you need. If a target uses an application-specific readiness condition, extend the handler with a bounded explicit wait for it. Selenium provides file, PNG-byte, and Base64 screenshot methods; save_screenshot returns false on an I/O error, so the sample checks the result. See the Selenium Python WebDriver API and Selenium WebDriver documentation.

4. Build, run locally, and deploy

  1. Choose the Lambda Python runtime tag, browser build, driver build, and target architecture. Keep versions pinned together.
  2. Complete the Dockerfile browser-install step and confirm the executable paths match CHROME_BINARY and CHROMEDRIVER_BINARY.
  3. Build for the function’s architecture. AWS’s example uses linux/amd64; use linux/arm64 when that is the architecture selected for the function and your browser build supports it.
  4. Run the image locally with AWS’s runtime interface emulator workflow and invoke the handler with a test event. Check startup, navigation, output dimensions, logs, and cleanup.
  5. Push the image to ECR and create or update the Lambda function to use that image. Publishing a new image to ECR alone does not update deployed function code; rebuild and update the function when the browser or driver changes.
  6. In the Lambda configuration, set memory and timeout based on measured behavior for your pages, and configure temporary storage if your browser workload needs more than the default. Test the deployed architecture and permissions.

For recurring synthetic monitoring rather than a custom screenshot endpoint, AWS CloudWatch Synthetics canaries provide browser automation using Playwright, Puppeteer, or Selenium WebDriver. See AWS synthetic monitoring documentation.

5. Configure capture behavior deliberately

Choice Sample behavior How to adapt it
Viewport 1280 × 800 CSS pixels Pass width and height in the event, keeping limits appropriate to your application.
Viewport or full page Current window/viewport PNG Selenium’s ordinary window screenshot is not a full-page capture guarantee. A full-page workflow needs browser-specific support or a separate implementation and validation.
Readiness Optional visible CSS selector; otherwise document ready state Use a site-specific selector or JavaScript condition for app-rendered content. Bound the wait.
Navigation and script timeout Derived from remaining invocation time, capped in the sample Keep enough time after navigation for waiting, encoding, output, and quit(). Tune against actual pages.
Output PNG encoded in a JSON response For durable or large outputs, write bytes to an object store and return a reference.
Browser profile and temp files Under /tmp Use unique per-invocation profile paths if concurrent browser processes may share a filesystem; ensure cleanup and sufficient temporary storage.

Other useful controls include page-load strategy, user agent, cookies, viewport scale, and browser preferences. Add them only when the capture requirement calls for them, and ensure custom headers or authentication credentials are not exposed in logs. Selenium’s options API describes browser capabilities and configuration: Selenium browser options.

6. Reliability, performance, and cost

  • Bound every wait. Navigation, selector waits, scripts, and retries should fit inside the Lambda deadline with time reserved for cleanup and response handling.
  • Always close WebDriver. The nested finally block calls driver.quit() even when navigation or image writing fails. Selenium’s getting-started example also closes the driver after use: Selenium getting started.
  • Expect variable page work. Third-party scripts, large resources, bot checks, and dynamic content affect runtime. No latency or cold-start benchmark is asserted here; measure using your browser build and target pages.
  • Keep the image focused. Browser binaries and shared libraries increase the container size. AWS permits at most 10 GB uncompressed including layers. Avoid unnecessary packages and rebuild intentionally.
  • Review invocation economics. Lambda charges depend on configured resources and execution time, while image storage and any output storage may add costs. Check current AWS pricing for your region and workload; this guide does not estimate a per-shot amount.
  • Control destinations and concurrency. If callers supply URLs, enforce an application-appropriate destination policy, cap viewport and wait settings, and set concurrency limits based on memory and browser process use.

7. Troubleshooting

Symptom Likely cause Fix
Chrome or driver executable not found Path mismatch, missing copy/install step, or missing execute permission Check the configured binary paths inside the built image and verify executable permissions.
Session not created or browser exits at startup Browser/driver version mismatch, unsupported architecture, missing shared library, or unsuitable startup flags Pin a compatible pair for the image architecture, inspect browser startup logs, and install the required libraries for the selected base image.
Chrome says it cannot create a profile or cache Profile points to a read-only or unavailable location, or concurrent processes reuse one profile Use a writable temporary path and a unique profile directory per process; remove temporary data afterward.
Invocation times out during navigation Slow page, stalled resource, or a timeout budget longer than the Lambda deadline allows Set a bounded page-load timeout, use a page-specific readiness signal, reserve cleanup time, and consider blocking unneeded resources if your design supports it.
Screenshot is blank or lacks application content Capture occurred before client-side rendering, content is behind a consent prompt, or the site returned a challenge/error page Wait for the actual content selector, inspect the loaded page and logs, and handle consent or challenge behavior according to the site and your use case.
Output is clipped or unexpectedly small Window dimensions are not the desired viewport, or the code assumed full-page behavior Set viewport dimensions explicitly and verify the resulting PNG. Implement and validate a separate full-page capture path if required.
Image save reports failure Temporary path or filesystem write issue Use a writable path, check free temporary storage, and inspect the Boolean return from save_screenshot.
New image is in ECR but Lambda still uses old behavior The function version/configuration was not updated to use the new image Update the Lambda function code to the published image and confirm the deployed image digest/version.
Works locally but fails in Lambda Architecture, OS libraries, permissions, environment, or runtime differ Build for the exact target architecture and exercise the same image through the Lambda runtime interface emulator, then validate in the deployed function.

8. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. A single request returns an image or PDF, without packaging Chrome and WebDriver in your Lambda image. See the ScreenshotNeo API documentation.

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}`);
  • Cookie banners are accepted and removed, along with known newsletter popups and chat widgets, before the shot.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers say the page verdict and billing status.
  • An MCP server lets AI agents use screenshot, page-info, and PDF-capture tools.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000.

Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month, with no card.

9. FAQ

Does Selenium’s screenshot method capture the whole page?

The ordinary window screenshot captures the current browser window. Treat full-page capture as a separate capability and verify the dimensions and content of the resulting image.

Can I use this for scheduled site checks?

Yes, a Lambda function can be invoked on a schedule, but if the primary goal is recurring synthetic monitoring, evaluate CloudWatch Synthetics and its browser automation runtimes.

Can one image target both Lambda architectures?

Build the function image for one target architecture. Choose the function architecture and browser build together, and publish the matching image.

Why does the guide leave browser installation as a build-specific step?

Browser binaries, drivers, shared libraries, base images, and architectures must be compatible. The cited documentation does not validate a particular bundled browser recipe, so pin and test the combination you select.