TL;DR
Headless Chromium launched through chrome-aws-lambda needs a handful of NSS libraries (libnss3.so and friends) that the serverless runtime does not provide. The package ships them inside an archive, but its last release only unpacks that archive when AWS_EXECUTION_ENV names Node.js 10, 12 or 14. On any newer runtime, and on Vercel with Fluid compute where that variable is hidden, the libraries are never extracted. Replace chrome-aws-lambda with @sparticuz/chromium, pin it to the Chromium major your puppeteer-core expects, and run on Node.js 22 or 24.
The error
The function works under next dev and fails the moment it runs in production. The log, reported verbatim on Stack Overflow by a developer calling Puppeteer from a Vercel API route, reads:
Error: Failed to launch the browser process!
/tmp/chromium: error while loading shared libraries: libnss3.so: cannot open shared object file: No such file or directory
TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
at onClose (/var/task/node_modules/puppeteer-core/lib/cjs/puppeteer/node/BrowserRunner.js:193:20)
The code behind it is the pattern many screenshot and Open Graph image routes still copy: puppeteer-core for the driver, chrome-aws-lambda for the binary, await chrome.executablePath as the path. Nothing in that code is wrong for the runtime it was written for. The runtime moved.
Why it happens
/tmp/chromium is not a browser you installed. It is the binary that chrome-aws-lambda decompresses from chromium.br into /tmp on the first invocation, because /tmp is the only writable directory in the function. The Linux dynamic loader then tries to resolve every shared library that binary links against. Most of them exist in the base image; the NSS set does not.
The package knows this and ships the missing libraries in a second archive, aws.tar.br, which contains lib/libnss3.so, lib/libnssutil3.so, lib/libsoftokn3.so, lib/libexpat.so.1 and a few others. The catch is the condition under which that archive is unpacked. In the chrome-aws-lambda source, both the LD_LIBRARY_PATH setup and the extraction of aws.tar.br sit behind the same test:
if (/^AWS_Lambda_nodejs(?:10|12|14)[.]x$/.test(process.env.AWS_EXECUTION_ENV) === true) {
// set LD_LIBRARY_PATH to /tmp/aws/lib and inflate aws.tar.br
}
When the function runs on Node.js 16 or anything later, that regular expression does not match. chromium.br is still decompressed, so /tmp/chromium exists, but /tmp/aws/lib never does and the loader never looks there. The result is exactly the message above: the binary is present, its first NSS dependency is not.
This is why the third answer on the question, "select Node.js 14 in the Vercel settings", worked at the time: it made the regex match. It is not an option today. Vercel's supported Node.js versions page lists 24.x (default), 22.x and 20.x only, and Node.js 20 is deprecated on Vercel from 1 October 2026. There is a second reason the old package cannot recover on Vercel: the Vercel Functions limits page states that with Fluid compute enabled, AWS_EXECUTION_ENV is not accessible to your code at all. The variable the check depends on is simply absent.
The accepted answer's advice about a Node.js 14.0.0 extract-zip bug concerns the full puppeteer package downloading its own browser. It does not apply to a puppeteer-core plus chrome-aws-lambda setup, which downloads nothing. The last chrome-aws-lambda release on the npm registry is 10.1.0, published in July 2021 with a peer dependency on puppeteer-core ^10.1.0; it will not be updated for current runtimes.
Fix
The maintained successor is @sparticuz/chromium, which began as a fork of chrome-aws-lambda and now only delivers the Chromium binary. It ships an al2023.tar.br archive with the Amazon Linux 2023 versions of the NSS libraries (libnss3.so, libnspr4.so, libsoftokn3.so and the rest), and its runtime detection checks for Node.js 20, 22 and 24 Lambda runtimes and, separately, for the VERCEL environment variable on Node.js 20 or later.
- Remove the old package so nothing still imports it:
npm uninstall chrome-aws-lambda puppeteer
- Pick the pair of versions.
@sparticuz/chromiumfollows Chromium majors, not semver, and its README warns that breaking changes can land in a patch release. Look up your driver on Puppeteer's supported-browsers table: at the time of writingpuppeteer-core25.11.0 drives Chrome 153, and the latest@sparticuz/chromiumon npm is 153.0.0. Pin both:
npm install --save-exact puppeteer-core@25.11.0 @sparticuz/chromium@153.0.0
- Set the Node.js major in
package.json. The current@sparticuz/chromiumdeclares"node": "^22.17.0 || >=24.0.0"in its engines field, so Node.js 20 is out regardless of the Vercel deprecation. Vercel readsengines.nodeand overrides the dashboard setting with it:
{
"engines": {
"node": "24.x"
}
}
- Rewrite the launch. In a Next.js App Router route handler (
app/api/og-shot/route.ts), pass Chromium's flags throughpuppeteer.defaultArgs, use"shell"as the headless mode (the package is built onchrome-headless-shell), and fall back to a local Chrome during development:
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
export const runtime = 'nodejs';
export const maxDuration = 60;
export async function POST(request: Request) {
const html = await request.text();
const onVercel = Boolean(process.env.VERCEL);
const browser = await puppeteer.launch({
args: onVercel
? await puppeteer.defaultArgs({ args: chromium.args, headless: 'shell' })
: [],
executablePath: onVercel
? await chromium.executablePath()
: process.env.LOCAL_CHROME_PATH,
headless: 'shell',
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(html, { waitUntil: 'networkidle0' });
const png = await page.screenshot({ type: 'png' });
return new Response(png, { headers: { 'Content-Type': 'image/png' } });
} finally {
await browser.close();
}
}
Closing the browser in finally matters: a warm instance is reused between requests, and a Chromium process left running from the previous request keeps holding the function's memory. The README asks for at least 512 MB and recommends 1600 MB or more; Vercel's default of 2 GB covers that.
- Leave bundling alone. Next.js already lists
@sparticuz/chromium,@sparticuz/chromium-min,puppeteer-coreandpuppeteerin its defaultserverExternalPackages, so they are required at runtime instead of being bundled. That is important: the package locates itsbin/archives by relative path, and a bundled copy cannot find them. Bundling a Node.js package into a server route breaks in other ways too; Module not found: Can't resolve 'encoding' on Vercel is Webpack failing on an optionalrequire()insidenode-fetch@2.
Verify the fix
Deploy a throwaway route that imports the package, extracts the binary and reports what the loader will see. Extraction writes the binary to /tmp/chromium and, when Vercel or an AL2023 Lambda runtime is detected, the NSS set to /tmp/al2023/lib:
import { existsSync } from 'node:fs';
import chromium from '@sparticuz/chromium';
export const runtime = 'nodejs';
export async function GET() {
const executablePath = await chromium.executablePath();
return Response.json({
node: process.version,
executablePath,
ldLibraryPath: process.env.LD_LIBRARY_PATH ?? null,
libnss3: existsSync('/tmp/al2023/lib/libnss3.so'),
});
}
A healthy response shows a Node.js 22 or 24 version, /tmp/chromium as the path, LD_LIBRARY_PATH starting with /tmp/al2023/lib, and libnss3: true. If libnss3 is false, the runtime was not detected: check that VERCEL is set and that the deployed Node.js major is 22 (22.17 or later) or 24, which is what the package's engines field requires. The detection code itself accepts 20 and up, but Node.js 20 falls outside that engines range. Then call the real route and confirm a PNG comes back. Remove the diagnostic route afterwards.
Edge cases the answers skip
The error comes back in Docker, not on Vercel. Inside a container you own, nothing unpacks libraries for you; the distribution must provide them. Puppeteer's troubleshooting page gives the diagnostic, ldd chrome | grep not, and a Debian package list that includes libnss3 and libnspr4. The second answer on the question turns that output into one apt-get install line in the image. The package names follow the base image's distribution, not the Node.js version: read them from /etc/os-release in the image rather than from the node: tag.
AWS Lambda on Graviton. The @sparticuz/chromium npm package contains x64 binaries only. For arm64 functions, the README directs you to @sparticuz/chromium-min with the arm64 layer or pack from the GitHub release. Shipping an x64 binary to an arm64 host fails differently, with the architecture mismatch explained in the exec format error on AWS Fargate.
Bundle size. Vercel caps a function at 250 MB uncompressed unless large functions are enabled. If Chromium pushes a route over, install @sparticuz/chromium-min instead and pass chromium.executablePath() the HTTPS URL of the pack tar from the matching GitHub release, hosted somewhere fast and close to the function; the package downloads and unpacks it to /tmp on a cold start and reuses it while the instance stays warm.
It works locally, fails only in production. That split is the signature of a runtime difference rather than a code bug, and it is worth ruling out the other usual suspects at the same time, which the postmortem on a Next.js build that passed while production broke walks through.
The underlying rule is the one the old error hid: a browser binary is only as portable as the shared libraries next to it. Pin the Chromium package to the driver, pin the Node.js major the package declares, and check libnss3 on the first deploy rather than on the first user request.
Originally published at https://www.iloveblogs.blog
Top comments (0)