ScreenshotNeo

BlogHow-to

How to Debug Puppeteer: Common Issues and Fixes

Debug Puppeteer by locating the failing layer first: Node.js, page code, or Chrome. Then use targeted fixes for launch failures, selector timeouts, containers, and Cloud Run.

By the ScreenshotNeo team4 October 202612 min read

Puppeteer failures usually come from one of three layers: your Node.js code, code running inside the page, or Chrome and its DevTools connection. Identify which layer owns the evidence before changing launch flags or increasing timeouts. Start by making the browser visible or slowing operations; then collect logs from the relevant layer. This guide covers the common launch, selector, Linux, Docker, and Cloud Run problems, with runnable examples.

The advice below follows the official Puppeteer debugging guide, troubleshooting guide, and page interactions guide. Those pages can change; record your installed Puppeteer and browser versions when investigating a regression.

1. Start with the failing layer

Write down the exact symptom and the operation that triggers it. A script that never launches Chrome needs different evidence from a script that launches but cannot find an element. Use this quick map:

Symptom Likely layer First evidence to collect
Chrome will not start or exits immediately Browser, operating system, or launch configuration Launch error, browser output, executable/cache path, OS and browser versions
Page appears wrong, blank, or incomplete Page code, navigation, or network Visible browser, page console messages, current URL and page content
Click or selector wait times out Page state or selector/action preconditions DOM state, selector validity, visibility and enabled state
Awaited operation never resolves Node/Puppeteer protocol communication Pending protocol errors and protocol logs
Script is unexpectedly slow in deployment Runtime or deployment configuration Whether work starts before or after the HTTP response; CPU allocation and cold start behavior

Capture the environment with the failure: Node version, Puppeteer version, browser build or channel, OS and container image, launch options, and whether the issue reproduces locally. Puppeteer guarantees compatibility with its bundled browser; using a system browser or alternate channel is at your own risk. See the LaunchOptions reference.

2. Make the failure observable

For a first pass, run a visible browser. If timing or animation makes the problem hard to observe, add slowMo. This complete example logs browser console messages and page errors, prints the current URL, and saves a screenshot when it fails.

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({
  headless: false,
  slowMo: 100,
  dumpio: true,
});

try {
  const page = await browser.newPage();
  page.on('console', msg => console.log('PAGE LOG:', msg.type(), msg.text()));
  page.on('pageerror', error => console.error('PAGE ERROR:', error));
  page.on('requestfailed', request => {
    console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText);
  });

  const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
  console.log('HTTP status:', response?.status() ?? 'no response');
  console.log('Current URL:', page.url());
  console.log('Title:', await page.title());
  await page.screenshot({ path: 'debug.png', fullPage: true });
} catch (error) {
  console.error('Puppeteer operation failed:', error);
  throw error;
} finally {
  await browser.close();
}

Save as debug.mjs, install Puppeteer with npm install puppeteer, then run node debug.mjs https://example.com. Remove headless: false and slowMo after you have diagnosed the issue. dumpio: true forwards browser process output to Node’s standard output and error streams.

Debug page code and Node code separately

  • Page code: browser-side console.log does not automatically appear in Node. Forward the page’s console event as in the example. For interactive debugging, launch with devtools: true and put a debugger; statement inside the function passed to page.evaluate().
  • Node code: run node --inspect-brk debug.mjs, launch with headless: false, and use Chrome or Chromium’s chrome://inspect/#devices to attach the debugger. Add a Node-side debugger; statement where execution should pause.
  • Protocol communication: run NODE_DEBUG="puppeteer:*" node debug.mjs to log DevTools protocol traffic. To inspect unresolved calls, log browser.debugInfo.pendingProtocolErrors.

Protocol logs can include sensitive information. Review and redact them before sharing. These debugging methods are described in the Puppeteer debugging guide.

3. Fix “Could not find expected browser locally”

Since Puppeteer v19, downloaded browsers are stored under ~/.cache/puppeteer, using the home directory. The error can mean the browser download did not happen, the process uses a different home directory, or the expected cache is unavailable in the runtime.

  1. Check that your dependency installation completed successfully and that the browser download is present in the environment where the script runs.
  2. Check which user runs Node and what home directory that user sees. A browser downloaded during one build or under one account might not be available to another runtime account.
  3. If the default cache location is unsuitable, set PUPPETEER_CACHE_DIR to a directory that exists and is readable by the runtime user. Keep the download and runtime paths consistent.
  4. In environments that cache dependencies between builds, check whether the install step that downloads the browser actually ran. Follow the deployment provider’s current guidance for cache behavior.
# Example: choose an explicit cache directory for this process
PUPPETEER_CACHE_DIR=/app/.cache/puppeteer node debug.mjs https://example.com

Do not assume the cache path is shared between local development, image builds, and production. The version-specific cache behavior is documented in Puppeteer’s troubleshooting guide.

4. Diagnose Chrome launch failures on Linux and in containers

A launch failure can have separate causes: missing shared libraries, sandbox restrictions, an unwritable profile directory, or container process and privilege behavior. Check them independently rather than changing several flags at once.

Check shared-library dependencies

On Linux, inspect the Chrome executable for missing libraries:

ldd /path/to/chrome | grep 'not found'

Use the path to the browser that Puppeteer actually launches. If libraries are missing, install the dependencies for your specific Linux distribution and image. Package names differ by distribution and image version, so do not copy a package list from a different base image without checking it.

Check sandbox and AppArmor restrictions

On Ubuntu 23.10 and later, an AppArmor profile can prevent Chrome for Testing from using user namespaces and result in No usable sandbox!. Check whether that restriction applies to the host and browser path; consult the Chromium AppArmor restrictions documentation for applicable workarounds.

Do not make --no-sandbox your routine fix. Puppeteer says, “Running without a sandbox is strongly discouraged.” Configure and run Chrome with a sandbox where possible. Treat disabling it as a security-relevant workaround requiring an explicit assessment of the environment.

Check the profile directory and runtime user

Chrome needs a writable user-data directory. Puppeteer normally creates a temporary profile, but an explicit profile directory can help isolate permission issues. Make sure the directory exists, is mounted writable, and is owned or writable by the account running Chrome.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  userDataDir: '/tmp/puppeteer-profile',
  dumpio: true,
});

try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
} finally {
  await browser.close();
}

Choose a profile location appropriate to your platform, and avoid sharing one profile directory between concurrent browser processes. If the error is permission-related, fix directory ownership or mount permissions rather than assuming a launch flag will resolve it.

Check container privileges and process cleanup

When developing locally, a container may lack privileges Chrome expects. Puppeteer’s troubleshooting guide suggests checking container privileges; it mentions --cap-add=SYS_ADMIN as a local diagnostic for some container setups, not as a universal production requirement. If Chrome child processes remain as zombies in Docker, dumb-init may help manage process termination. Verify both suggestions against your container’s security model and deployment environment.

5. Debug Alpine and browser-version mismatches

The Puppeteer troubleshooting page warns that Chrome does not support Alpine out of the box. An Alpine image may need compatible system dependencies, and the exact combination must be tested with the specific image and browser build. The same page calls out timeout issues with the Chromium version in Alpine 3.20; keep that warning tied to that distribution and version rather than generalizing it to all Alpine images or current Chromium releases.

If a failure began after upgrading Puppeteer, Chrome, or the base image, record those versions and the launch options before changing flags. Puppeteer is guaranteed to work with its bundled browser; a system Chrome or alternate channel may not be compatible. See the LaunchOptions documentation and the current troubleshooting guide.

6. Fix selector and interaction timeouts

A waitForSelector timeout means the requested selector did not appear within the configured time. Increasing the timeout is useful only when the page legitimately needs longer. First establish whether the selector is correct, whether the page reached the expected state, and whether the element is present but hidden, disabled, or outside the expected frame.

Prefer Locators for interactions

Puppeteer recommends Locators for selecting and interacting with elements. A locator waits for the element and action preconditions, such as visibility, enabled state, viewport presence, and a stable bounding box before a click. Set a timeout per locator when a particular operation needs more time:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  await page.locator('button[type="submit"]')
    .setTimeout(10000)
    .click();
} finally {
  await browser.close();
}

A locator timeout can mean the element was not found or the action preconditions were not satisfied in time. Diagnose which one applies before changing the timeout. Locator configuration can tune checks such as viewport presence, visibility, enabled state, and stable bounding box; disable a check only when the page behavior calls for it. See Page interactions.

Use waitForSelector when you need a lower-level wait

const element = await page.waitForSelector('.result', {
  visible: true,
  timeout: 10000,
});

if (!element) {
  throw new Error('The result was expected to be visible');
}

try {
  await element.click();
} finally {
  await element.dispose();
}

waitForSelector supports visible, hidden, timeout, and an AbortSignal. Its default timeout is 30 seconds, configurable with page.setDefaultTimeout(); pass timeout: 0 to disable it, but an unbounded wait can leave work stuck indefinitely. A hidden wait can resolve with null if the selector is absent. If the call returns an ElementHandle, dispose of it when finished. Unlike a Locator action, a lower-level wait does not automatically retry the action after it fails. Consult the waitForSelector API reference.

Selector-timeout checklist

  • Log page.url() after navigation; redirects or an unexpected route can put the script on a different page.
  • Check the HTTP response status and page console or request failures to see whether the page loaded the expected application state.
  • Confirm the selector matches the live DOM and is scoped to the correct frame. CSS selectors are accepted directly; Puppeteer also supports selector syntax for text, accessibility attributes, XPath, and shadow DOM.
  • Decide whether you need presence, visibility, or disappearance. A DOM element can exist without being visible.
  • For clicks and fills, prefer a Locator so action preconditions are handled. If a Locator still times out, determine which precondition is unmet.
  • Use a longer timeout only when you have evidence that the page’s legitimate load time exceeds the current value.

7. Investigate slow Puppeteer jobs on Google Cloud Run

This is a Cloud Run-specific behavior documented by Puppeteer: by default, Cloud Run disables CPU after an HTTP response is written. If your handler sends a response and then starts Puppeteer in the background, browser launch or subsequent work can appear extremely slow. Launch and perform the required work before sending the response, or enable always-allocated CPU for genuine background processing.

import express from 'express';
import puppeteer from 'puppeteer';

const app = express();

app.post('/capture', async (req, res, next) => {
  let browser;
  try {
    browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    const title = await page.title();
    res.json({ title });
  } catch (error) {
    next(error);
  } finally {
    await browser?.close();
  }
});

app.listen(3000);

If the work must continue after acknowledging the request, configure Cloud Run’s CPU allocation for that workload. The default Cloud Run Node.js runtime also does not include all system packages needed for headless Chrome, so the documented setup requires a Dockerfile with the needed dependencies. Keep runtime CPU behavior and browser dependencies as separate checks. See Puppeteer’s Cloud Run troubleshooting notes.

8. Common errors and their fixes

Error or symptom Likely cause What to check or change
Could not find expected browser locally Browser cache missing, unavailable, or installed for another home directory Confirm the browser download and runtime user; set PUPPETEER_CACHE_DIR to a persistent, readable location if needed.
No usable sandbox! Sandbox or user-namespace restrictions, including a possible Ubuntu AppArmor policy Check the host policy and Chromium’s AppArmor guidance; prefer a working sandbox configuration.
Chrome exits on Linux with missing library messages Required shared libraries are absent from the OS image Run ldd on the actual Chrome executable; install dependencies for that distribution and image.
Chrome cannot create or use its profile Profile directory is missing or not writable by the runtime user Check mount and ownership; select a writable userDataDir if necessary.
waitForSelector throws TimeoutError Wrong selector, unexpected navigation/state, hidden element, wrong frame, or genuinely slow content Inspect URL, response, DOM, frame, visibility and console/network evidence; use a Locator for actions.
Click times out although an element exists Locator action preconditions, such as enabled state, visibility, viewport position, or stable geometry, were not met Inspect the element’s state and page layout; adjust the relevant Locator check only if appropriate.
Cloud Run task becomes slow after responding CPU is disabled after response under the default allocation behavior Do work before the response or enable always-allocated CPU for background work.
Chrome child processes remain in Docker Container process reaping and PID 1 behavior Inspect process shutdown and consider dumb-init where appropriate.
Failure appeared after a browser or Puppeteer upgrade Browser build/channel and Puppeteer version may not be a supported pair Record versions, OS and launch options; reproduce with the bundled browser before changing flags.

9. Performance, reliability, and cost considerations

Performance

Visible mode, slowMo, protocol logging, and full-page screenshots are diagnostic aids; they add overhead and should not be treated as production performance settings. For repeatable results, wait for the page condition your task actually needs rather than relying on a long fixed delay. Avoid globally disabling timeouts: it hides stalls and can leave jobs occupying workers indefinitely.

Reliability

Close the browser in a finally block so errors do not leave browser processes running. Capture enough context to reproduce the issue: versions, launch options, URL, response status, relevant console and request errors, and whether the failure is local or deployment-specific. Keep browser cache and profile directories writable and consistent for the runtime account. Redact sensitive page data and protocol logs before sharing them.

Cost

For self-hosted Puppeteer, account for the compute and storage used by the Node.js process, browser processes, downloaded browser build, and any retained artifacts. The supplied Puppeteer documentation does not establish a universal cost or performance benchmark; actual resource use depends on pages, concurrency, and deployment configuration. Cloud Run background execution may require always-allocated CPU, which changes the service’s compute configuration and associated cost. Check your cloud provider’s current pricing for your configuration.

10. Or skip the browser setup

If the goal is to capture a website screenshot rather than debug browser automation, ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. A single GET request returns PNG, JPEG, WebP, or PDF. The API and its options are documented at ScreenshotNeo docs.

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,
)
r.raise_for_status()
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 import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

Cookie banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free for 1,000 screenshots a month, with no card.

11. Frequently asked questions

How do I debug Puppeteer when I cannot see the browser?

For local investigation, set headless: false and optionally add slowMo. In a headless deployment, forward page console messages and browser process output to Node logs, then save a screenshot at the point of failure.

Should I always increase the selector timeout?

No. First check whether the selector, frame, navigation, and page state are correct. Increase a timeout only when the intended element genuinely needs longer to appear.

Is --no-sandbox a safe default in Docker?

No. Puppeteer strongly discourages running without a sandbox. Investigate the container’s sandbox configuration and security constraints before considering that workaround.

Can Puppeteer use a system-installed Chrome?

It can be configured to use an alternate executable or channel, but Puppeteer guarantees compatibility with its bundled browser. Record both browser and Puppeteer versions when troubleshooting an alternate browser.

Sources