Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Webhook Pricing Docs Blog Log In Sign Up
Back to Blog

Fix "Could Not Find Expected Browser (Chrome) Locally" in Puppeteer

Puppeteer cannot find Chrome? Fix the "could not find expected browser locally" error in Docker, CI/CD, serverless, monorepos, and local development.

Fix "Could Not Find Expected Browser (Chrome) Locally" in Puppeteer

Puppeteer installed, everything looks good, but when you run your script — Chrome is not there. The error says "could not find expected browser locally" and suggests running npm install again, which doesn't help. The problem is that Puppeteer downloads Chrome separately from the main package, and in a lot of environments that download silently fails or gets lost. Docker, CI pipelines, serverless, monorepos — each one breaks it in its own way. Below I'll go through every case I've seen and show how to fix each one.

The error itself looks like this:

Error: Could not find expected browser (chrome) locally.
Run `npm install` to download the correct Chromium revision (1045629).

The irony is that the error tells you exactly what to do. You run npm install. Nothing changes. You delete node_modules, reinstall everything from scratch. Same error. At this point you start questioning whether computers are real.

I have hit this error in pretty much every environment you can think of: GitHub Actions after a runner update, Docker containers with multi-stage builds, AWS Lambda where Chrome literally doesn't fit, pnpm monorepos where the postinstall script never ran. Each time the cause was different, but the fix was usually one command once I knew which one.

Below I cover every scenario I've encountered and the fix for each. If you're in a hurry, jump to the one-command fix section and see if that solves it. If not, find your environment below.

If Chrome is found but refuses to start (missing libraries, sandbox errors), that's a different problem. We have a separate guide for failed to launch browser process. And if Chrome starts but crashes mid-script, check our page crashed guide.

What Puppeteer Is Actually Looking For

Before jumping into fixes, it helps to understand what's going on behind the scenes. When you npm install puppeteer, two things happen. The JavaScript library lands in node_modules/ like any other package. But then a postinstall script quietly handles the Puppeteer browser download — a specific build of Chromium gets tucked into a cache folder.

Before Puppeteer 19, that cache folder lived inside node_modules/puppeteer/.local-chromium/. Since version 19, it moved to ~/.cache/puppeteer/ on Linux and macOS, or %LOCALAPPDATA%\puppeteer on Windows. This change alone broke thousands of deployments overnight, but more on that later.

When you run your script, Puppeteer checks that cache folder for a Chrome binary matching a specific revision number. If anything is off — folder empty, wrong revision, folder missing entirely — you get the Puppeteer chrome not found error.

Quick way to see what Puppeteer expects versus what's actually on disk:

npx puppeteer browsers list

Empty output means Chrome was never downloaded. A revision that doesn't match what your Puppeteer version expects means a version mismatch. Either way, now you can see the problem instead of guessing.

The One-Command Fix That Works Most of the Time

Before going down the rabbit hole of environment-specific fixes, try the simple thing first:

npx puppeteer browsers install chrome

That command tells Puppeteer to download the exact Chrome version it needs, right now, into the correct cache folder. No npm reinstall, no cache clearing, no config files. Just run your script again after this.

If that worked, great — you're done. The rest of this post explains why Chrome went missing and how to make sure it doesn't happen again. If you got a permission error, a proxy timeout, or something else went wrong, keep reading. The remaining causes are all things I've seen before.

You Have puppeteer-core, Not puppeteer

More common than you'd think. Check your package.json. If it says puppeteer-core instead of puppeteer, that's your whole problem.

Two packages, one critical difference. The regular puppeteer package downloads Chrome automatically when you install it. The puppeteer-core package skips the download entirely — it's meant for situations where you bring your own browser. Maybe you have Chrome already installed on the machine, maybe you want to connect to a remote browser, maybe you're on Lambda with a custom Chromium build. Whatever the reason, if you use puppeteer-core, you need to tell it where Chrome lives:

const puppeteer = require('puppeteer-core');

const browser = await puppeteer.launch({
  executablePath: '/usr/bin/google-chrome-stable'
});

The path depends on your OS. On macOS it's /Applications/Google Chrome.app/Contents/MacOS/Google Chrome. On Windows, usually C:\Program Files\Google\Chrome\Application\chrome.exe. On Linux servers, install Chrome with apt install google-chrome-stable first.

If you don't have a specific reason to use the core version, just switch to puppeteer. It handles everything for you.

Your CI Caches node_modules but Not Chrome

CI/CD pipelines are the most common cause, and the frustrating part is that the error message gives you zero clues about it.

Here's the scenario. Your first CI build runs npm install, which downloads both the Puppeteer library (into node_modules/) and Chrome (into ~/.cache/puppeteer/). Your CI config caches node_modules/ to speed up future builds. Makes sense. But the next build loads node_modules from cache, decides npm install isn't needed because everything looks fresh, and never triggers the postinstall script that downloads Chrome. Puppeteer is there. Chrome is not.

I've fixed this two ways, and both work fine.

Option one: cache both folders. In GitHub Actions:

- name: Cache Puppeteer browser
  uses: actions/cache@v4
  with:
    path: ~/.cache/puppeteer
    key: puppeteer-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
  run: npm ci

Option two, which I prefer because it's simpler: force Chrome to download after every install, regardless of cache.

- name: Ensure Chrome is downloaded
  run: npx puppeteer browsers install chrome

About 10 seconds added to your build. Puppeteer skips the download if Chrome already exists, so it's basically free on cache hits. But you never have to think about cache keys matching up again.

GitLab CI and CircleCI have the same problem. The fix is always the same pattern: either cache the browser folder too, or run the install command explicitly.

Docker Builds That Forget the Browser

I once spent an hour debugging a production deploy that worked perfectly on my laptop. The screenshot script ran fine locally, the Docker image built without errors, but the container crashed on startup with "could not find expected browser." Turned out my multi-stage build was copying node_modules/ to the production stage but leaving Chrome behind in /root/.cache/puppeteer/.

Docker loses Chrome in two ways. First is multi-stage builds, like my case above. You install everything in one stage, then cherry-pick files for the final image. If your COPY doesn't include the Puppeteer cache, Chrome stays behind:

# BAD — copies node_modules but Chrome is in /root/.cache/puppeteer
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

FROM node:20-slim
WORKDIR /app
COPY --from=build /app .
CMD ["node", "index.js"]   # Chrome is missing

The cleanest fix is to tell Puppeteer to store Chrome inside your project directory. Create a .puppeteerrc.cjs file:

// .puppeteerrc.cjs — tell Puppeteer to store Chrome inside the project
const { join } = require('path');
module.exports = {
  cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};

Now when you COPY the project, Chrome comes with it. Problem gone.

The second way Docker loses Chrome is user mismatches. If you build the image as root but run the container as node or some custom user, Chrome sits in /root/.cache/puppeteer/ while Puppeteer looks in /home/node/.cache/puppeteer/. Set PUPPETEER_CACHE_DIR to a shared location:

ENV PUPPETEER_CACHE_DIR=/opt/puppeteer-cache
RUN npm ci && npx puppeteer browsers install chrome

A complete Dockerfile that handles both issues plus the system libraries Chrome needs. If Chrome is found but won't start because of missing libs, see our failed to launch browser guide.

FROM node:20-slim

RUN apt-get update && apt-get install -y \
  libnss3 libatk1.0-0 libatk-bridge2.0-0 \
  libcups2 libdrm2 libxkbcommon0 \
  libxcomposite1 libxdamage1 libxrandr2 \
  libgbm1 libasound2 libpango-1.0-0 libcairo2 \
  && rm -rf /var/lib/apt/lists/*

ENV PUPPETEER_CACHE_DIR=/opt/puppeteer-cache

WORKDIR /app
COPY package*.json .puppeteerrc.cjs ./
RUN npm ci

COPY . .
CMD ["node", "index.js"]

Serverless Functions Where Chrome Doesn't Fit

AWS Lambda limits your deployment package to 250 MB. A standard Chromium build weighs around 300 MB. It doesn't fit, and no amount of configuration will change that.

The workaround is @sparticuz/chromium, a stripped-down Chromium build made specifically for Lambda. It pairs with puppeteer-core (not the regular puppeteer, since you don't want it trying to download full Chrome):

npm install puppeteer-core @sparticuz/chromium
const chromium = require('@sparticuz/chromium');
const puppeteer = require('puppeteer-core');

exports.handler = async () => {
  const browser = await puppeteer.launch({
    args: chromium.args,
    executablePath: await chromium.executablePath(),
    headless: chromium.headless,
  });

  const page = await browser.newPage();
  await page.goto('https://example.com');
  const screenshot = await page.screenshot();
  await browser.close();

  return {
    statusCode: 200,
    body: screenshot.toString('base64'),
    isBase64Encoded: true,
  };
};

Vercel has a subtler gotcha. Running vercel dev locally sets VERCEL=1 in your environment. If your code checks that variable to decide which Chrome to load, it'll pick the Lambda version on your laptop and crash. Check AWS_LAMBDA_FUNCTION_NAME instead, or use your own IS_PRODUCTION flag.

Google Cloud Functions had it worst. When Puppeteer 19 moved the cache directory, it broke every existing Cloud Function deployment overnight. The Puppeteer team closed the GitHub issue (#9131, 292 comments) as "not planned." 292 comments. All frustrated. The official answer was basically "figure it out yourself." The fix: set PUPPETEER_CACHE_DIR to a path your function can write to, usually /tmp:

PUPPETEER_CACHE_DIR=/tmp/puppeteer npx puppeteer browsers install chrome

Honestly, running headless Chrome in serverless environments feels like fighting the platform. These services are built for small, fast functions. Chrome is neither. If your end goal is screenshots or PDFs rather than full browser automation, there's a simpler path. More on that at the end.

pnpm, Yarn PnP, and Monorepo Gotchas

npm runs postinstall scripts by default. pnpm and Yarn are pickier about it, and that's where things go wrong.

With pnpm, Puppeteer's browser download might not run at all because pnpm isolates dependencies more strictly. The Chrome binary can end up in pnpm's content-addressable store, which isn't a path Puppeteer knows to check at runtime. If npx puppeteer browsers install chrome gives you "command not found," use pnpm's own exec command:

pnpm exec puppeteer browsers install chrome

Yarn with Plug'n'Play (PnP) has a different issue entirely. PnP replaces node_modules with a virtual filesystem, and Puppeteer's postinstall script doesn't know what to do with that. The Yarn team recommends marking Puppeteer as "unplugged" in .yarnrc.yml:

# .yarnrc.yml
packageExtensions:
  puppeteer@*:
    unplugged: true

Monorepos add another layer. The root npm install might download Chrome, but the workspace that actually uses Puppeteer runs from a completely different directory. The cache path resolves relative to wherever install ran, not where your code runs. Fix it with an absolute PUPPETEER_CACHE_DIR that works from any workspace:

export PUPPETEER_CACHE_DIR=/home/ci/.puppeteer-browsers

The Version 19 Cache Migration

If you upgraded Puppeteer and suddenly hit "could not find expected browser locally," this is almost certainly why.

Before version 19, Chrome lived inside your project at node_modules/puppeteer/.local-chromium/. Version 19 moved it to a user-level cache: ~/.cache/puppeteer/ on Linux, ~/Library/Caches/puppeteer/ on macOS. The reasoning made sense. Reinstalling node_modules would no longer wipe your Chrome download. But it also broke every deploy script, Docker build, and CI cache that assumed Chrome was part of the project.

Quick check:

npx puppeteer --version

On 19 or higher, Chrome is in the user cache. On 18 or below, it's in node_modules. If you just upgraded, run npx puppeteer browsers install chrome to populate the new location.

Also worth checking: does your deploy process set PUPPETEER_SKIP_DOWNLOAD=true somewhere? Or the older name, PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true? Someone might have added it to speed up CI installs and forgotten about it. That flag tells Puppeteer to skip Chrome entirely. Search your environment variables, .npmrc, and CI config. Removing it and reinstalling usually fixes things.

Proxy and Firewall Blocks

This one is sneaky. Corporate proxies can silently kill the Chromium download during npm install. The postinstall script fails, but it doesn't crash the install. Puppeteer just gets installed without Chrome. You don't find out until later when your code actually tries to launch a browser.

Set your proxy before installing:

export HTTP_PROXY=http://your-proxy:8080
export HTTPS_PROXY=http://your-proxy:8080
npm install puppeteer

Or put it in .npmrc so you don't have to remember every time:

proxy=http://your-proxy:8080
https-proxy=http://your-proxy:8080

Some corporate networks block storage.googleapis.com entirely — that's where Puppeteer downloads Chromium from. If that's your situation, download Chrome manually on a machine with internet access, copy the binary to your server, and point Puppeteer to it with executablePath or PUPPETEER_CACHE_DIR.

A Configuration File That Prevents This Forever

After seeing "could not find expected browser locally" across enough projects, I started adding a .puppeteerrc.cjs file to every repo that uses Puppeteer. It tells Puppeteer where to store Chrome, and because the file lives in the project root, it travels with your code to every environment:

// .puppeteerrc.cjs
const { join } = require('path');

/**
 * @type {import("puppeteer").Configuration}
 */
module.exports = {
  cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};

With this file, Chrome downloads into .cache/puppeteer/ inside your project. CI caches it automatically. Docker copies it with your code. Deploy scripts include it without extra config. Add .cache/ to your .gitignore (you don't want a ~300 MB binary in your repo), and you're set.

One thing to watch in monorepos: the path resolves relative to the config file's location. If multiple workspaces use Puppeteer, an absolute path via PUPPETEER_CACHE_DIR is safer than a relative one.

Six Commands to Debug "Could Not Find Expected Browser" Errors

If none of the scenarios above match your situation, run through these in order. They'll tell you exactly what's wrong:

# 1. Where does Puppeteer expect Chrome?
npx puppeteer browsers list

# 2. Is Chrome actually there?
ls -la ~/.cache/puppeteer/chrome/

# 3. Which Puppeteer version?
npx puppeteer --version

# 4. puppeteer or puppeteer-core?
cat package.json | grep puppeteer

# 5. Is the download being skipped?
env | grep -i puppeteer

# 6. Force Chrome download now
npx puppeteer browsers install chrome

Steps 1 and 2 together show you the gap between expectation and reality. Step 5 catches hidden environment variables like PUPPETEER_SKIP_DOWNLOAD that someone set once and forgot about. Step 6 fills the gap.

When You're Done Debugging Chrome Downloads

Take a step back and look at what this error made you do. Configure cache directories. Write Docker stages. Set environment variables for CI. Debug package manager postinstall scripts. Maybe even dig through 292-comment GitHub issues. All because Puppeteer needs a ~300 MB browser binary in exactly the right place on disk.

If your actual goal is taking screenshots or generating PDFs — not automating browser interactions like filling out forms or navigating multi-step flows — you can skip all of this. A single HTTP request does the same thing without any local Chrome:

curl "https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com&format=png&full_page=true" \
  -H "Authorization: Bearer YOUR_API_KEY"

No Chrome to install. No cache directory to manage. Works the same from Lambda, Docker, GitHub Actions, or your laptop.

ScreenshotRun handles Chrome, the system libraries, and the rendering on its servers. You send a URL, you get back an image. It supports full-page captures, PDF export, dark mode, custom viewports, and multiple formats (PNG, JPEG, WebP, AVIF).

We have integration guides for Node.js, Python, and cURL if you want to see what the workflow looks like in your language.

To be clear: this only makes sense for screenshots and PDFs. If you're automating logins, running visual regression tests, or scraping content across pages, Puppeteer is the right tool and there's no shortcut. If you're weighing the tradeoffs, we wrote an honest Puppeteer vs screenshot API comparison.

Frequently Asked Questions

Puppeteer expects a Chrome binary in its cache directory (~/.cache/puppeteer/ on Linux/macOS since version 19). If the postinstall script that downloads Chrome was skipped, interrupted, or the cache was cleared, the binary is missing and Puppeteer throws this error. Run npx puppeteer browsers install chrome to download it.

Run npx puppeteer browsers install chrome in your project directory. This downloads the exact Chrome version Puppeteer needs into the correct cache folder. If you are using pnpm, run pnpm exec puppeteer browsers install chrome instead.

The puppeteer package downloads a compatible Chrome browser automatically during npm install. The puppeteer-core package does not download any browser — you must provide the path to an existing Chrome installation via the executablePath launch option. If you just need Puppeteer to work out of the box, use puppeteer, not puppeteer-core.

Since Puppeteer 19, Chrome is stored in ~/.cache/puppeteer/ on Linux and macOS, or %LOCALAPPDATA%\puppeteer on Windows. Before version 19, it was stored inside node_modules/puppeteer/.local-chromium/. You can check the exact location by running npx puppeteer browsers list.

In Docker, Chrome often gets lost during multi-stage builds or when running as a non-root user. Set PUPPETEER_CACHE_DIR to a shared path (like /opt/puppeteer-cache) in your Dockerfile, and make sure your COPY commands include the cache directory. Alternatively, use a .puppeteerrc.cjs file to store Chrome inside your project directory so it gets copied with your code.

Your CI probably caches node_modules but not the Puppeteer browser cache (~/.cache/puppeteer/). Either add that path to your cache configuration, or add a step that runs npx puppeteer browsers install chrome after npm install. The second approach is simpler and adds about 10 seconds to your build.

Use puppeteer-core with @sparticuz/chromium instead of the regular puppeteer package. Standard Chromium is too large for Lambda's 250 MB limit. The @sparticuz/chromium package provides a stripped-down build that fits within Lambda constraints.

Puppeteer 19 moved the Chrome cache from node_modules/puppeteer/.local-chromium/ to ~/.cache/puppeteer/. After upgrading, run npx puppeteer browsers install chrome to populate the new location.

More from the blog

View all posts
Fix Puppeteer "Browser Has Disconnected" Error (All Causes)

Fix Puppeteer "Browser Has Disconnected" Error (All Causes)

Read more →
Why Instagram Breaks Your Screenshots (And How to Fix It with Playwright)

Why Instagram Breaks Your Screenshots (And How to Fix It with Playwright)

Instagram shows two login popups, triggers React re-renders when you hide them wrong, and locks scrollHeight to the viewport. Three problems, three fixes for Playwright and Puppeteer.

Read more →
Fix Failed to Launch the Browser Process in Puppeteer

Fix Failed to Launch the Browser Process in Puppeteer

Every cause of Puppeteer browser launch failures with tested fixes for each environment.

Read more →