Fix /tmp/chromium libnss3.so Error on Vercel and Lambda
Get headless Chromium launching again in a Vercel Function or on AWS Lambda, and understand why pinning Node 14 was never a real fix.
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:
bashnpm 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:bashnpm 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:json{ "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:tsimport 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
finallymatters: 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.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
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.
Further reading: Fix Vercel 'Git author must have access to the project'.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix next-auth /api/auth/error in production: NO_SECRET
Reproduced on next-auth 4.24.15 and Next.js 15.5.28: why sign-in works on localhost but fails after deploy, what a 404 versus a 500 at /api/auth/error tells you, and the env change that fixes it.
Fix Vercel 'Git author must have access to the project'
Work out which of the three access rules blocked your commit, fix the author identity or the team setup, and get pushes deploying again.
Next.js Middleware Not Running on Vercel: 3 Causes
Middleware that works locally can disappear in Vercel production because the matcher never matches, Edge bundles a Node-only import, or auth middleware cannot read the token. This guide gives you the diagnosis order I use before touching app code.


