ScreenshotNeo

BlogHow-to

How to Handle Timeouts in Puppeteer Browser Management

Find the failing Puppeteer operation first, then set a timeout or wait condition that matches it. Includes runnable JavaScript examples and fixes for common failures.

By the ScreenshotNeo team4 October 20269 min read

Start by identifying the exact Puppeteer call that timed out. Browser startup, navigation, selector waits, and connecting to or managing an existing browser have separate controls. Raising a timeout only allows that operation to wait longer; it does not fix a missing browser, an incorrect selector, an unsuitable wait condition, or a navigation race.

The examples below use JavaScript with Puppeteer. Puppeteer documents a 30-second default for browser startup and common wait options; check the documentation for your installed version because APIs and environment requirements can change. See the LaunchOptions reference and WaitForOptions reference.

1. Identify which operation timed out

Read the error and stack trace to find the failing call. Record the Puppeteer and browser versions, whether the browser is local or remote, and the URL or selector involved. That information distinguishes an installation or startup problem from a page-level wait.

Failing operation What timed out First thing to check
puppeteer.launch() Browser process startup Browser executable, install scripts, runtime dependencies, permissions, and launch timeout
page.goto() or waitForNavigation() Navigation or its chosen lifecycle condition URL, redirects, server response, and whether waitUntil matches the task
page.waitForSelector() A selector did not meet the requested condition Selector, frame, visibility, and application readiness
puppeteer.connect() or browser management Connection or a managed browser operation WebSocket endpoint, remote browser health, and who owns its lifecycle

2. A runnable baseline with bounded waits

Install Puppeteer in a Node.js project, then save this as capture.mjs. Puppeteer normally downloads a compatible browser during installation. If your package manager blocks install scripts, the official troubleshooting guide documents npx puppeteer browsers install as the manual installation command. See Puppeteer troubleshooting.

import puppeteer from 'puppeteer';

const controller = new AbortController();
const outerDeadline = setTimeout(() => controller.abort(), 90_000);
let browser;

try {
  // This timeout controls startup, not navigation or selector waits.
  browser = await puppeteer.launch({
    headless: true,
    timeout: 30_000,
    signal: controller.signal,
  });

  const page = await browser.newPage();
  page.setDefaultNavigationTimeout(20_000);
  page.setDefaultTimeout(10_000);

  const response = await page.goto('https://example.com', {
    waitUntil: 'domcontentloaded',
    timeout: 20_000,
    signal: controller.signal,
  });

  // Navigation completion does not necessarily mean app content is ready.
  await page.waitForSelector('h1', {
    visible: true,
    timeout: 10_000,
    signal: controller.signal,
  });

  console.log({ status: response?.status(), title: await page.title() });
} finally {
  clearTimeout(outerDeadline);
  if (browser) await browser.close();
}

The outer deadline provides a bound for the overall task, while the per-operation values help identify where time is spent. In production, handle an abort as cancellation and ensure cleanup runs. An abort signal is available in Puppeteer wait options; the launch options also accept a signal. If you use timeout 0, Puppeteer’s own timeout is disabled, so provide another deadline or cancellation mechanism.

3. Set the timeout at the right scope

Browser startup

puppeteer.launch({ timeout }) controls how long Puppeteer waits for the browser to start. The documented default is 30,000 milliseconds; 0 disables this limit. If startup exceeds the limit, verify that the expected browser is installed and runnable before increasing it.

const browser = await puppeteer.launch({ timeout: 45_000 });

Timeout is only one launch option. Other relevant options include executablePath for a specific browser executable, channel for a standard Chrome installation, headless, and signal for cancellation. Puppeteer says it is only guaranteed to work with its bundled browser; using a system browser is at your own risk. Check the LaunchOptions reference for the supported options in your version.

page.setDefaultNavigationTimeout(ms) sets the default for goBack(), goForward(), goto(), reload(), setContent(), and waitForNavigation(). An operation-level timeout can override the default for an exceptional case. See setDefaultNavigationTimeout().

page.setDefaultNavigationTimeout(25_000);

await page.goto('https://example.com/report', {
  waitUntil: 'domcontentloaded',
  timeout: 40_000,
});

Use the lightest lifecycle condition that represents success:

  • domcontentloaded waits for the document to be parsed.
  • load waits for the page load event and is the default for wait options.
  • networkidle waits for network activity to settle according to Puppeteer’s lifecycle event definition. Pages with polling, analytics, or long-lived requests may not reach it.

You can pass one condition or an array. With an array, all listed events must fire. A stricter condition can add delay without improving the result your task needs. The WaitForOptions reference describes waitUntil, the default, timeout, and signal.

General page waits and selectors

page.setDefaultTimeout(ms) changes the general page wait default, including selector waits. A selector wait can also set its own timeout, visibility condition, and abort signal. The default timeout is 30,000 milliseconds; 0 disables it.

page.setDefaultTimeout(12_000);

// Element exists in the DOM:
await page.waitForSelector('[data-testid="results"]', { timeout: 8_000 });

// Element exists and is visible:
await page.waitForSelector('[data-testid="results"]', {
  visible: true,
  timeout: 12_000,
});

visible: true means the selector must resolve to a visible element; hidden: true waits for the selected element to be hidden or absent. Choose the condition that describes the task. A selector wait does not prove that navigation completed, and navigation completion does not prove that client-rendered content is ready.

4. Wait for the event that means the task succeeded

Choose a wait based on what your automation needs next: navigation, a selector, a response, network idle, or an application-specific function condition. If a page keeps background requests open, waiting for network idle can time out even though the content you need is already present. Prefer a task-specific readiness signal when one is available.

A click that triggers navigation

Register the navigation wait before performing the click. Awaiting the click first and attaching the navigation wait afterward can miss a fast navigation. Puppeteer documents using Promise.all() for this pattern; see waitForNavigation().

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 20_000 }),
  page.click('a.next-page'),
]);
console.log('Navigation response:', response?.status());

A client-side History API transition may resolve navigation with a null response because there may be no new main-resource response. If the action updates content without a navigation, wait for the resulting selector or state instead.

Wait for application state

When the page is a single-page app, the relevant success signal may be a result element, a changed URL, a response, or a condition evaluated in the page. For example, if the app updates a known status attribute, wait for that state rather than every network request to stop.

await page.waitForFunction(
  () => document.querySelector('[data-testid="status"]')?.textContent?.trim() === 'Ready',
  { timeout: 15_000 },
);

Confirm that the condition is specific enough to indicate usable content. A wait for an element that exists in a hidden template, for example, may finish too early if the actual requirement is a visible result.

5. Browser startup and installation checks

  1. Confirm which package you use. puppeteer downloads a browser; puppeteer-core does not and is intended for a browser you manage or connect to.
  2. Check that the browser binary exists in the runtime environment and that the process user can execute it.
  3. Check system dependencies and permissions for your operating system or container. Puppeteer’s system requirements lists current requirements.
  4. If install scripts were blocked, install the browser explicitly with npx puppeteer browsers install.
  5. For managed browser paths, configure executablePath or the appropriate channel and verify compatibility with your Puppeteer version.

Increasing the launch timeout may help when startup is genuinely slow, such as in a constrained environment, but it cannot install a missing browser or supply missing system libraries.

6. Connecting to and managing an existing browser

A remotely managed browser has a different lifecycle from one created with launch(). Connect using its WebSocket endpoint, then decide whether your script owns the browser process. Puppeteer distinguishes these operations:

  • browser.disconnect() detaches Puppeteer without shutting down the browser or closing its pages.
  • browser.close() gracefully closes the browser.

Use disconnect when another service owns the browser and expects it to keep running. Use close when your script launched and owns the browser. See Puppeteer’s browser management guide.

import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
});

try {
  const pages = await browser.pages();
  console.log(`Connected; open pages: ${pages.length}`);
} finally {
  // Detach only. The external browser remains running.
  browser.disconnect();
}

Do not apply launch-timeout advice to a connection failure. Check that the endpoint is reachable and current, credentials are correct if required by your provider, and the remote browser is accepting connections. Treat a remote page navigation timeout as a separate page-level issue.

7. Troubleshooting common Puppeteer timeouts

Symptom Likely cause Fix
Launch reports a timeout or cannot find Chrome Browser download was skipped, cache path differs, or binary is not runnable Install the browser with npx puppeteer browsers install; check cache configuration, executable path, permissions, and system requirements.
goto() times out on a page that appears loaded The chosen waitUntil condition is stronger than the task needs, or background requests stay active Use a suitable lifecycle condition such as domcontentloaded, then wait for the specific content needed.
waitForSelector() times out Selector is wrong, target is in another frame, element is absent, or visibility condition is unmet Inspect the rendered DOM and frame; verify selector spelling and whether presence or visibility is required.
Click succeeds but navigation wait times out Navigation wait started after the click, or the action only changed app state Use the concurrent Promise.all() pattern; if there is no navigation, wait for the resulting state or selector.
Remote browser disconnects or times out Endpoint is stale or unreachable, provider/browser unavailable, or lifecycle ownership is unclear Verify the endpoint and remote service health; use disconnect() or close() according to ownership.
Raising the timeout only makes jobs take longer to fail The underlying condition never becomes true Fix the selector, navigation condition, browser installation, or application readiness signal. Keep a bounded deadline.
A job hangs after setting timeout to zero Puppeteer’s timeout was disabled without an outer bound Restore a finite timeout or add an AbortSignal/overall deadline and cleanup path.

8. Reliability, performance, and cost considerations

Timeouts are part of a task’s resource policy. A very short limit can fail under transient server or startup delays; an overly long or disabled limit can tie up workers while a condition that will never occur continues waiting. Use finite per-operation limits, an overall job deadline, cancellation where supported, and a cleanup path that closes only browsers your code owns.

For performance, avoid waiting for more page activity than the result requires. A targeted selector or application-ready condition may finish sooner than a full load or network-idle condition. Reuse a browser process when your workload and isolation requirements allow it, while keeping page and context cleanup explicit. Measure your own workloads; this guide makes no performance benchmark claim.

Cost depends on where the browser runs and how long workers remain occupied. Longer limits do not make a request succeed; they can increase resource occupancy on a self-managed runner or remote browser service. Track timeout rates by operation and distinguish launch, navigation, selector, and connection failures so infrastructure costs and fixes can be tied to the cause.

9. Or skip the browser setup

For a screenshot without managing a Puppeteer browser, ScreenshotNeo provides a website screenshot API and MCP server. Its one-call API returns an image or PDF; see the ScreenshotNeo API documentation.

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)
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}`);

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card.

10. Frequently asked questions

Does raising Puppeteer’s timeout fix a slow website?

It gives the operation more time. It does not improve the site or guarantee that the wait condition will happen. Confirm the condition and server behavior first.

Can I set one timeout for everything?

There are separate startup, navigation, and general page-wait scopes. Set a default only when it suits that scope; use a per-operation value for exceptions.

Why does waitForSelector() time out after navigation succeeds?

Navigation and selector readiness are different events. The page can finish navigating without rendering the target selector, or the selector may be in a different frame or not visible.

Should I use networkidle for every page?

No. Pages with ongoing requests may not become idle. Use it only when network quiescence is a meaningful success condition for the task.

What is the difference between disconnecting and closing?

Disconnect detaches Puppeteer and leaves the browser and pages open. Close shuts down the browser.