Your Puppeteer script takes a clean screenshot on your laptop, then throws before Chrome even opens the moment you deploy it to AWS Lambda. By the end of this you'll know the three things that break headless Chrome on Lambda, how to fix each one, and the point where one HTTP call beats maintaining the fix.
The short version: the normal puppeteer package won't run on Lambda, because the Chromium it downloads is neither in the runtime nor small enough to ship. You pair puppeteer-core with @sparticuz/chromium, raise the function's memory, and accept a slower cold start. That works. The other option is to not run a browser at all: a screenshot API like ScreenshotRender renders the page on its own Chromium and returns a hosted image, so your function ships zero browser code. This post covers both, and when each is the right call.
Why does a Puppeteer screenshot fail on AWS Lambda?
It fails because the Chromium your code expects is not there. The default puppeteer package downloads a full browser to your machine on install, and that binary lives in your local node_modules, not in the Lambda runtime. When the function boots, there is no browser to launch, so puppeteer.launch() throws.
The obvious reflex, bundling the browser into your deployment, hits a wall too. AWS caps a Lambda deployment package at 250 MB unzipped, including every layer, and a normal Chromium plus its shared libraries does not fit. So you have two problems stacked on top of each other: the browser is missing, and the straightforward way to add it is too big for the package. That pairing is why a script that runs fine locally dies in the cloud.
The fix is a different Chromium and a different Puppeteer, which is the next section.
How do you run headless Chrome on AWS Lambda?
You run it by pairing puppeteer-core with @sparticuz/chromium. The first is Puppeteer without the bundled browser download, and the second is a trimmed, compressed Chromium built specifically to fit inside a Lambda package and boot from it.
puppeteer-core is the same API you already know, minus the install step that pulls down a browser, so nothing tries to download Chromium at deploy time. @sparticuz/chromium is the maintained successor to the old chrome-aws-lambda project; it brotli-compresses a stripped Chromium so it clears the 250 MB ceiling and decompresses it when the function cold starts. You point puppeteer-core at the executable path the package exposes, launch with the flags it ships, open the page, and call page.screenshot(). The screenshot line itself is identical to any other Puppeteer script; only the launch changes. If you want the capture options themselves, our Puppeteer screenshot guide covers fullPage, element clips, and waiting.
At this point it works, and most tutorials stop here. The problem is that "it works" on Lambda is not the same as "it keeps working," and the reasons why are the part nobody puts in the quickstart.
What still breaks after Chrome runs on Lambda?
Three things break after the happy path: cold starts, memory and timeout limits, and version pinning. The first two you feel immediately; the third is the one that bites you weeks later during an unrelated upgrade.
- Cold starts. Every cold invocation has to decompress and launch Chromium before it can render anything, which adds real latency on top of the normal Lambda cold start. You can smooth it with provisioned concurrency, but that keeps instances warm around the clock and you pay for them whether or not a screenshot is requested.
- Memory and timeout. The 128 MB default memory is nowhere near enough for a browser, and because Lambda scales CPU with memory, a low setting is both fragile and slow. The default timeout of a few seconds is also too short for a cold boot plus a page load, so both dials have to go up together.
- Version pinning.
@sparticuz/chromiumties each release to a specific Chromium version and a compatiblepuppeteer-coreversion. Bump one without the other and the launch silently breaks. This is the maintenance tax: a dependency upgrade that has nothing to do with screenshots can take your capture function down.
None of these are dealbreakers on their own. Stacked together, they are the difference between a demo and a service you are on call for.
Skip the Chromium build, the layer size math, and the cold starts.
One GET request to ScreenshotRender returns a hosted PNG with cookie banners and ads already stripped, rendered by a real Chromium on our side. Your Lambda ships zero browser code. 100 free screenshots, no credit card.
Get an API keyWhen should you skip Lambda and call a screenshot API?
Skip the browser when you only need an image of a public URL and would rather not own Chromium in production. Keep driving the browser yourself when you need to script a login, fill forms, or interact with the page before the shot. That is the whole decision.
The honest tradeoff is time. Running headless Chrome on Lambda is a solved problem, but "solved" means you now own a compressed browser binary, a memory and timeout profile, cold start latency, and a version lock between two packages. For a team whose product is not screenshots, that is a lot of infrastructure to carry for a PNG. An API moves all of it off your plate: no binary in the package, no launch flags, no drift when you upgrade an unrelated dependency. You trade a per request cost for never touching browser plumbing again. If you are still weighing providers, we broke down how to choose a screenshot API separately.
The case for staying on Lambda is real too. If your capture needs a logged in session or a sequence of clicks, a URL only API cannot reach that page, and driving the browser is the right tool. For everything public, it is overkill.
How do you take an AWS Lambda screenshot without a browser?
You call a screenshot API over plain HTTP from inside the function, so there is no Chromium in your bundle at all. With ScreenshotRender the entire capture is one request your Lambda can make with a normal fetch: https://screenshotrender.com/api/v1/screenshot?apiKey=YOUR_API_KEY&url=https://en.wikipedia.org/wiki/HTTP&fullPage=true. The JSON response carries a hosted image URL at data.screenshot, plus the page title, description, and a tokensUsed count.
The parameters are the whole surface. url is the page to capture, fullPage=true grabs the entire scrollable document instead of the default 1280 by 720 viewport, and wait takes a delay in seconds for pages that keep rendering after the initial load. Because it is just an HTTP GET, the same call works from any Lambda runtime, Node.js, Python, or anything that can make a request, without an SDK. A serverless function that does nothing but forward a URL and return the resulting image URL stays tiny, cold starts fast, and never needs a memory bump for a browser it no longer runs.
Two things you would otherwise script yourself are handled server side. Cookie consent banners, ad overlays, and chat widgets are removed before every capture, on every plan including the free one, so you are not writing per site dismissal logic. And you only spend a credit on a successful render: failed requests do not count against your quota. For bot protected pages, Stealth Mode on the Hobby plan and up renders past common challenges, the same problem that makes a plain headless browser fail on Cloudflare sites.
Common questions about AWS Lambda screenshots
Can AWS Lambda run Puppeteer?
Yes, but not the default puppeteer package. That package downloads a full Chromium on install, which is not present in the Lambda runtime and is too large to ship in the deployment package. You install puppeteer-core, which has no bundled browser, and pair it with @sparticuz/chromium, a stripped and compressed Chromium built to fit inside a Lambda layer. Together they run headless Chrome in the function.
Why is my Lambda deployment package too large for Chromium?
AWS Lambda caps a deployment package at 250 MB unzipped, including all layers, and a normal Chromium binary plus its dependencies blows past that. @sparticuz/chromium exists to solve exactly this: it brotli-compresses a trimmed Chromium so it fits under the limit and decompresses it at cold start. If you tried to bundle full puppeteer, you hit the size limit because the browser download is included.
How do I fix a Lambda timeout when taking a screenshot?
Raise the function timeout and the memory together. Lambda's default timeout is only a few seconds, which is not enough for a cold Chromium boot plus a page load, so set it higher to give the render room. Chromium is also memory hungry, and on Lambda more memory means more CPU, so a function stuck at low memory both runs out of RAM and renders slowly. Start at 1,024 MB or more and measure.
Does Playwright work on AWS Lambda?
It can, but it is heavier to deploy than Puppeteer. Playwright installs its own browser builds and system dependencies, which are awkward to fit into a Lambda package, so most people who run it on Lambda use a container image rather than a zip and layer. If you only need a screenshot of a public URL, both Puppeteer and Playwright are more infrastructure than a single API call requires.
How much memory does headless Chrome need on Lambda?
More than the 128 MB Lambda default. Chromium rendering a real page comfortably uses several hundred megabytes, and because Lambda scales CPU with memory, a low setting is slow as well as fragile. A common starting point is 1,024 MB to 1,600 MB, then tune based on the pages you capture. Heavier pages with many images and fonts need more.
The honest rule: if your Lambda needs to log in, click, or fill a form before the shot, run headless Chrome and pay the maintenance to keep it alive. If it just needs an image of a public page, a one line HTTP call keeps the function tiny and takes the browser, the layer math, and the cold starts off your plate for good.



