How to Capture Website Screenshots with Selenium in a Headless Chrome Serverless Function
Build a serverless screenshot function with Selenium and headless Chrome: package the browser, wait for page readiness, save artifacts, and troubleshoot deployment.
Direct answer: package a Chrome binary and a compatible ChromeDriver with your serverless function, enable Chrome’s headless mode, direct its writable profile and cache to temporary storage, navigate with Selenium, wait for an explicit page-ready condition, set the viewport, capture the screenshot, then persist or return the image before the invocation ends. On AWS Lambda, use a compatible container image, layer, or deployment artifact and use /tmp for scratch files. Pin Chrome and ChromeDriver together and validate the whole setup in the cloud runtime.
This guide uses Python and AWS Lambda for a concrete example. The same lifecycle applies to other serverless platforms, but packaging, runtime limits, and storage paths vary. Headless Chrome runs without a visible UI; since Chrome 112, headless and headful modes share the main Chrome implementation. Chrome’s headless documentation describes the current mode. [c001]
1. Choose the execution model
Use a request-triggered function when a caller needs a screenshot on demand. Use a scheduled monitoring service when the job is recurring and the main goal is checking a site over time. Compare platform limits and artifact handling before choosing:
| Option | Useful when | Plan for |
|---|---|---|
| AWS Lambda with Selenium | You need a request-triggered function or application-specific integration. | Packaging compatible browser binaries and Linux dependencies; invocation time, memory, and temporary storage; saving artifacts outside the invocation environment. |
| Cloud Run container | You want to package Chromium and dependencies in a container. | Container image maintenance and validation of Selenium behavior in the chosen image. Google documents Chromium installation and webpage screenshots as a headless-browser use case, though its cited page does not provide a Selenium recipe. [c003] |
| Amazon CloudWatch Synthetics | You want recurring browser monitoring with screenshot artifacts. | Canary workflow and its supported browser model. AWS documents Python and Node.js canaries using Selenium WebDriver and Chrome, scheduled as frequently as once per minute; Selenium canaries support Chrome only. [c006] |
AWS Lambda’s documented standard invocation maximum is 900 seconds. Function memory ranges from 128 MB to 10,240 MB; ZIP package contents are limited to 250 MB unzipped including layers, and container images to 10 GB uncompressed. These are platform quotas, not recommended settings or assurances that a browser fits. Chrome consumes memory and CPU, and Lambda allocates CPU in proportion to configured memory. [c004]
Lambda’s /tmp storage is configurable from 512 MB to 10,240 MB in 1-MB increments. It is unique to an execution environment, so treat it as scratch space, not durable artifact storage. [c005]
2. Package Chrome and ChromeDriver
- Select a Lambda runtime and deployment format compatible with your browser build and Python dependencies.
- Bundle Chrome and ChromeDriver, or use a maintained layer or container that supplies them. Ensure both binaries are built for the target Linux environment and keep their versions compatible.
- Configure Chrome’s binary path if it is not on the default path. Use
--headlessand direct profile and cache data to writable temporary directories such as/tmp. - Pin browser and driver versions together, then update them together. Chrome’s old headless implementation became a separate
chrome-headless-shellbinary starting with version 132.0.6793.0; verify which binary your packaging source provides. [c001] - Run the function in the actual cloud runtime before relying on local results. System libraries, architecture, permissions, and available scratch space can differ.
Older AWS examples demonstrate the packaging problem, but their Chrome 60, ChromeDriver 2.33, Java 8, and Python 3.6 values are historical. Do not copy their versions or legacy flags into a current deployment. [c007] [c008]
3. Runnable Python capture handler
The handler below assumes your deployment artifact provides Selenium, Chrome, ChromeDriver, and any required Linux libraries. Set CHROME_BINARY and CHROMEDRIVER to the installed paths if they are not on the default search path. It uses an explicit readiness selector rather than assuming that a fixed delay works for every site.
import base64
import os
import tempfile
from pathlib import Path
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
def lambda_handler(event, context):
url = event["url"]
ready_selector = event.get("ready_selector")
width = int(event.get("width", 1440))
height = int(event.get("height", 1000))
wait_seconds = int(event.get("wait_seconds", 30))
profile_dir = tempfile.mkdtemp(prefix="chrome-profile-", dir="/tmp")
cache_dir = tempfile.mkdtemp(prefix="chrome-cache-", dir="/tmp")
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument(f"--user-data-dir={profile_dir}")
options.add_argument(f"--disk-cache-dir={cache_dir}")
options.add_argument(f"--window-size={width},{height}")
chrome_binary = os.environ.get("CHROME_BINARY")
if chrome_binary:
options.binary_location = chrome_binary
driver_path = os.environ.get("CHROMEDRIVER")
service = Service(executable_path=driver_path) if driver_path else Service()
driver = webdriver.Chrome(service=service, options=options)
driver.set_page_load_timeout(wait_seconds)
try:
driver.get(url)
if ready_selector:
WebDriverWait(driver, wait_seconds).until(
lambda browser: browser.find_elements("css selector", ready_selector)
)
else:
WebDriverWait(driver, wait_seconds).until(
lambda browser: browser.execute_script(
"return document.readyState"
) in ("interactive", "complete")
)
png = driver.get_screenshot_as_png()
# Replace this return with an upload to durable object storage when needed.
return {
"statusCode": 200,
"headers": {"content-type": "image/png"},
"isBase64Encoded": True,
"body": base64.b64encode(png).decode("ascii"),
}
finally:
driver.quit()
The example returns a viewport PNG encoded for a gateway-style response. For larger images or durable retention, upload the bytes to object storage and return a reference or signed link. Use a unique profile per invocation so concurrent or reused execution environments do not share browser state accidentally. Ensure the handler’s timeout leaves enough time for browser shutdown and artifact upload.
4. Set readiness, viewport, and screenshot scope
Wait for the page you need
driver.get() waits according to WebDriver’s page-load strategy, but that alone may not mean a single-page app has rendered the content you need. Prefer an app-specific marker or a known element. If the page has no stable selector, wait for a bounded condition such as a particular DOM state. Set both the navigation timeout and explicit wait timeout, and handle timeout exceptions. A fixed sleep can be useful for a known animation delay, but it is not a general readiness test.
Chrome’s command-line --timeout and --screenshot flags describe Chrome CLI behavior, not Selenium wait semantics. In Selenium, use WebDriver waits and its screenshot methods. [c002]
Choose the viewport and scope
The --window-size=WIDTH,HEIGHT argument defines the browser window dimensions used by this example. A regular Selenium screenshot captures the visible viewport. Do not assume it captures the entire document. Full-page capture requires a separate technique that is specific to the selected browser and Selenium setup; validate that technique in the deployed runtime and account for very tall pages, sticky elements, and lazy-loaded images.
Set viewport dimensions before navigation if responsive layout behavior matters. To capture a mobile layout, choose a mobile-sized viewport and, where needed, configure device emulation through Chrome DevTools Protocol. Screenshot dimensions and page layout depend on the selected browser build and options.
5. Return or persist the image
- Return bytes: suitable for a small synchronous response when the gateway supports the response size and binary encoding format.
- Upload an artifact: suitable for larger images or retention beyond the invocation. Write to a unique key, then upload to durable object storage and return a reference.
- Use asynchronous work: suitable when browser startup, navigation, rendering, or uploads may exceed the caller’s response window. Store job state and provide a completion mechanism.
Lambda’s /tmp contents belong to the execution environment and should not be treated as durable or shared across all invocations. AWS’s historical Selenium example wrote screenshots to S3, illustrating durable artifact storage; it is an example rather than a current service requirement. [c005] [c007]
6. cURL, Node.js, and Python alternatives
The deployment-specific Selenium handler above is Python. These direct Selenium examples show the same core sequence in cURL and Node.js contexts where they apply: cURL invokes an already-deployed screenshot endpoint; Node.js can use Selenium WebDriver in a function whose image includes compatible Chrome and ChromeDriver. They do not remove the browser packaging requirement.
Invoke your deployed endpoint with cURL
curl -X POST "https://YOUR_FUNCTION_ENDPOINT" \
-H "content-type: application/json" \
-d '{"url":"https://example.com","ready_selector":"h1","width":1440,"height":1000}' \
--output screenshot.png
Python Selenium handler core
The complete Lambda handler above is the Python implementation. If the function is not behind a gateway, call its platform-specific invocation interface and adapt the event/response wrapper; the browser setup and explicit wait remain the same.
Node.js Selenium example
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const fs = require('node:fs/promises');
async function capture(url, outputPath, readySelector) {
const options = new chrome.Options();
options.addArguments(
'--headless',
'--no-sandbox',
'--disable-dev-shm-usage',
'--window-size=1440,1000',
'--user-data-dir=/tmp/chrome-profile',
'--disk-cache-dir=/tmp/chrome-cache'
);
if (process.env.CHROME_BINARY) options.setChromeBinaryPath(process.env.CHROME_BINARY);
const service = new chrome.ServiceBuilder(process.env.CHROMEDRIVER);
const driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.setChromeService(service)
.build();
try {
await driver.manage().setTimeouts({ pageLoad: 30000 });
await driver.get(url);
if (readySelector) {
await driver.wait(until.elementLocated(By.css(readySelector)), 30000);
} else {
await driver.wait(async () =>
(await driver.executeScript('return document.readyState')) === 'complete',
30000
);
}
const image = await driver.takeScreenshot();
await fs.writeFile(outputPath, Buffer.from(image, 'base64'));
} finally {
await driver.quit();
}
}
capture('https://example.com', '/tmp/screenshot.png', 'h1')
.catch((error) => { console.error(error); process.exitCode = 1; });
For concurrent Node.js invocations in a reused environment, generate unique profile and cache paths rather than using shared fixed paths. Install and lock the Selenium package version that matches your application’s runtime requirements.
7. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, with options documented in the API docs. For a simple WebP capture:
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo is made by Yorker Media; see ScreenshotNeo for product details. Sign up for 1,000 free screenshots a month with no card.
8. Performance, reliability, and cost
Performance
- Browser startup and page rendering both contribute to invocation time. Reuse a browser only when your execution model safely controls session isolation, cleanup, and concurrent requests.
- Set memory based on observed workload needs; Lambda allocates CPU in proportion to memory. Tune by measuring in the target runtime instead of assuming a universal setting. [c004]
- Keep screenshots at the smallest viewport and format that meet the use case. Large full-page images increase render time, memory use, response size, and upload time.
- Allocate
/tmpfor binaries and working files with headroom. Lambda allows 512 MB to 10,240 MB, but the correct amount depends on packaged binaries and workload. [c005]
Reliability
- Pin and update Chrome and ChromeDriver as a pair; run a cloud-runtime smoke capture after changing either.
- Use bounded navigation and readiness waits, validate the requested URL, and return actionable errors for timeouts and missing browser binaries.
- Always quit the driver in a
finallyblock. Use unique temporary directories and clean them up if the environment is reused. - Persist output to durable storage before returning success. Do not expose partial files as completed captures.
- Give the function enough time for navigation, readiness, capture, upload, and cleanup. The 900-second Lambda ceiling is a maximum, not a target. [c004]
Cost
The dossier establishes platform limits but contains no current price comparison. Estimate cost using the selected platform’s current pricing and your measured memory allocation, invocation duration, request volume, storage, and data transfer. Browser work can make invocations longer and memory-heavy; no single configuration is cheapest for all workloads. For recurring checks, compare custom-function operation with CloudWatch Synthetics using current pricing and your required schedule. [c004] [c006]
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Chrome fails to start | Binary missing, wrong architecture, missing shared libraries, or an invalid executable path. | Confirm the binary exists and runs in the target image; set its path explicitly; package runtime libraries for the selected base image. |
| ChromeDriver reports a session creation or version error | Driver and browser builds are incompatible. | Pin compatible versions and deploy them together. Check the actual browser binary included in the artifact. |
| Permission denied or profile cannot be created | Chrome is writing to a read-only path or a shared profile. | Point profile and cache directories at writable temporary storage such as a unique directory under /tmp. |
| Browser crashes under load | Insufficient memory, exhausted temporary space, concurrent sessions sharing files, or oversized pages. | Measure memory and storage use, increase configured resources within platform limits, isolate profiles, reduce concurrency, or capture a smaller viewport. |
| Screenshot is blank or missing content | Capture occurs before app rendering, navigation reached an error/bot page, or a selector did not match the intended content. | Wait for a stable application marker, inspect the final URL and page state, and report non-ready results instead of silently returning an image. |
| Navigation or explicit wait times out | Slow or blocked site, long-running requests, or a readiness selector that never appears. | Use bounded timeouts, choose a marker that exists for the target page, and handle timeout as a failed capture. Avoid increasing limits without checking the function’s remaining time. |
| Image works locally but not in the cloud | Different OS libraries, permissions, architecture, browser version, or writable paths. | Build and test against a Linux-like target environment and verify in the deployed runtime. |
| Artifact disappears after success | Image was left in /tmp, which is temporary and environment-specific. |
Upload it to durable object storage before returning a successful response. |
| Gateway rejects the response | Binary response encoding or response-size limits do not match gateway configuration. | Configure binary handling and base64 response semantics, or upload the image and return a reference. |
10. FAQ
Does headless Chrome render differently from normal Chrome?
Current Chrome shares the main implementation between headless and headful modes from Chrome 112 onward, though your deployed build, flags, and environment still matter. [c001]
Can I use the Chrome CLI screenshot flag from Selenium?
The CLI’s --screenshot flag is for command-line capture. Selenium code should invoke WebDriver screenshot methods after its own readiness checks. [c002]
Can one Lambda invocation capture many URLs?
It can, but each navigation consumes time and resources. Bound the number per invocation, isolate failures per URL, and leave time to persist outputs and close the browser.
When should I use a managed canary instead?
Consider CloudWatch Synthetics when you need scheduled browser monitoring and screenshot artifacts. AWS documents Selenium and Chrome support for its canaries. [c006]


