ScreenshotNeo

BlogHow-to

How to Fix Puppeteer and Pyppeteer Timeouts

Find the operation that timed out, choose the right wait condition, and scope a reliable timeout in Puppeteer or Pyppeteer.

By the ScreenshotNeo team1 October 20267 min read

Fix the operation that timed out before changing the number. A navigation timeout, selector timeout, request or response timeout, locator timeout, and test-runner deadline have different causes and fixes. Read the complete stack trace, identify the rejected await, verify that the expected event can happen, then set a timeout only for that operation.

The message Navigation timeout of 30000 ms exceeded points toward navigation or lifecycle assumptions only when it comes from page.goto() or waitForNavigation(). The same word, timeout, can also come from waitForSelector(), a locator, a network wait, or your test framework.

1. Identify the failing await

Start with the rejected call and its arguments. Log the URL and target condition while reproducing the failure.

try {
  console.log({ url, selector: '[data-ready="true"]' });
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
  await page.waitForSelector('[data-ready="true"]', { timeout: 20_000 });
} catch (error) {
  console.error('browser wait failed', {
    message: error.message,
    url: page.url(),
  });
  throw error;
}
Rejected operation What to check first Typical scoped fix
goto, reload, goBack, goForward, waitForNavigation Redirects, connectivity, lifecycle condition, pages with long-running requests Per-navigation timeout or navigation default
waitForSelector or locator Selector, frame, shadow root, visibility, application state Correct selector/state and per-wait timeout
waitForResponse or waitForRequest URL predicate, method, timing, request actually occurring Register the wait before the action and scope its timeout
Click or other action Element readiness, overlays, navigation race Locator/action wait or coordinated event wait
Whole test or job Runner deadline independent of browser settings Adjust the runner only after browser waits are correct

Ask whether the awaited event should happen at all. A wrong route, authentication redirect, consent dialog, missing feature flag, or selector in another frame will not be repaired by waiting longer.

2. Puppeteer: use operation-specific timeouts

Current Puppeteer Page documentation separates navigation defaults from the general wait default. setDefaultNavigationTimeout() applies to navigation methods including goto, reload, history navigation, setContent, and waitForNavigation. setDefaultTimeout() changes the general default used by waits and actions. waitForSelector documents a 30,000 ms default and accepts a per-call timeout; timeout: 0 disables that wait timeout. See the Puppeteer Page API.

Complete Puppeteer example

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  const url = 'https://example.com';

  try {
    // Keep this larger limit local to the slow navigation.
    await page.goto(url, {
      waitUntil: 'domcontentloaded',
      timeout: 60_000,
    });

    // Wait for the application state the script actually needs.
    await page.waitForSelector('h1', {
      visible: true,
      timeout: 20_000,
    });

    console.log(await page.title());
  } finally {
    await browser.close();
  }
})();

Choose the navigation condition deliberately

  • domcontentloaded waits for the document to be parsed. It can be sufficient when later resources are irrelevant.
  • load waits for the page load event, including resources required for that event.
  • networkidle0 and networkidle2 depend on network activity and can be unsuitable for applications that keep connections open.

There is no universally correct lifecycle event. Select the condition that proves your task is ready, then wait for an application-specific element or response when that is the real requirement.

Set defaults only when they describe the page

page.setDefaultNavigationTimeout(60_000);
page.setDefaultTimeout(20_000);

Use a navigation default when several legitimate navigations on the page need the same bound. Use the general default for shared non-navigation waits. Keep these settings near page setup so a later test does not inherit an unexplained limit. Do not set every timeout to zero to hide hangs.

3. Pyppeteer: verify the installed version

Pyppeteer 0.0.25 documents a 30-second default for goto(), a per-call millisecond timeout, waitUntil (defaulting to load), and setDefaultNavigationTimeout(). Its selector waits also document a 30-second default. The published reference is old, so inspect your installed package and confirm that its option names and browser version match your code. Pyppeteer also recommends using its bundled Chromium version when possible. Consult the Pyppeteer API reference.

Complete Pyppeteer example

import asyncio
import pyppeteer

async def main():
    browser = await pyppeteer.launch(headless=True)
    page = await browser.newPage()
    url = 'https://example.com'

    try:
        await page.goto(
            url,
            {
                'waitUntil': 'domcontentloaded',
                'timeout': 60_000,
            },
        )
        await page.waitForSelector(
            'h1',
            {'timeout': 20_000},
        )
        print(await page.title())
    finally:
        await browser.close()

asyncio.run(main())

Check the package version with python -m pip show pyppeteer. If a timeout appears with a launch, browser disconnect, or protocol error, investigate that runtime problem separately; a longer page wait cannot repair an incompatible or disconnected browser.

4. Wait for the state you need

A page can finish load while its application is still fetching data. Conversely, a page can keep analytics or streaming requests open after the element you need is ready. Prefer a selector, URL, response, or application state that proves the next operation can run.

Selector and locator waits

await page.waitForSelector('[data-ready="true"]', {
  visible: true,
  timeout: 20_000,
});

If this expires, inspect await page.url() and the current DOM. Confirm spelling, visibility requirements, iframe context, shadow DOM, and whether the page reached the expected route. Puppeteer locators can wait for an element and its actionable state; use a per-locator timeout when one control is slower than the rest. See the Puppeteer interactions guide.

Response and request waits

const responsePromise = page.waitForResponse(
  response => response.url().endsWith('/api/report') && response.status() === 200,
  { timeout: 20_000 },
);
await page.click('[data-load-report]');
const response = await responsePromise;
console.log(await response.json());

Register the wait before the action that causes the request. Make the predicate specific enough to avoid resolving on an unrelated request.

Clicks that cause navigation

A click-triggered navigation and a separately awaited waitForNavigation() can race. Coordinate both promises so the listener is active before the click, as warned in Puppeteer’s Page documentation.

await Promise.all([
  page.waitForNavigation({
    waitUntil: 'domcontentloaded',
    timeout: 30_000,
  }),
  page.click('a.next'),
]);

Use this only when the click performs a real navigation. For client-side routing, wait for the resulting URL, selector, or response instead.

5. Troubleshooting checklist

  • Print the final URL and inspect redirect and authentication behavior.
  • Confirm the network is reachable from the machine running Chromium.
  • Choose a lifecycle event that matches the task instead of habitually using a network-idle condition.
  • Check whether long-lived connections keep the chosen condition from completing.
  • Increase only the navigation timeout if the page is slow but valid.

Selector or locator timeout

  • Verify the selector in the actual DOM at the time of failure.
  • Check whether the element is inside an iframe or shadow root.
  • Remove visible: true only if hidden elements are genuinely acceptable.
  • Wait for the application state that creates the element.
  • Do not use a large timeout to conceal a selector that never appears.
  • Use the Promise.all pattern when a document navigation is expected.
  • If the site uses client-side routing, wait for its URL, selector, or API response.
  • Check for overlays or disabled controls that prevent the click.

Pyppeteer behaves differently from examples

  • Record the installed Pyppeteer version and Chromium revision.
  • Use the argument style documented by that version.
  • Compare bundled-browser compatibility before changing page timeouts.

The test runner reports a timeout

Many runners impose a deadline around the entire test. A browser wait can be correctly configured while the enclosing test still expires first. Identify the runner’s rejected operation, then make its deadline large enough to contain the intentionally scoped browser waits. Keep the runner limit finite so a broken page fails visibly.

6. Performance, reliability, and cost

  • Performance: waiting for a precise selector or response usually finishes sooner than waiting for every network request. Avoid arbitrary sleeps because they add fixed latency and still fail on slower runs.
  • Reliability: log the URL, selector or predicate, lifecycle condition, and installed browser version. Keep retries outside the browser wait and retry only errors that are plausibly transient.
  • Limits: finite, scoped timeouts expose broken routes and missing events. A disabled timeout can leave workers occupied indefinitely.
  • Concurrency: close pages and browsers in finally blocks. Leaked pages can exhaust memory and make later operations appear to time out.
  • Cost: self-hosted Puppeteer and Pyppeteer use your compute and browser resources. Hosted capture can replace browser maintenance when you only need an image or PDF.

7. Or skip the browser setup

If your goal is a clean screenshot or PDF rather than browser automation, ScreenshotNeo provides a single request to capture a URL. It handles the browser lifecycle and exposes wait, device, CSS, JavaScript, blocking, authentication, caching, bulk, PDF, and webhook options through its API. Read the ScreenshotNeo API documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers identify the page verdict and billing result. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account and start with the 1,000 monthly screenshots.

8. FAQ

Should I set every timeout to 60 seconds?

No. Match the timeout to the operation and fix wrong selectors, frames, routes, and lifecycle assumptions first.

Does load mean the application is ready?

No. It means the document’s load event fired. Dynamic applications often need a selector, response, or other state check afterward.

When is timeout: 0 appropriate?

Only when an intentionally unbounded wait is safe and another mechanism guarantees termination. It is not a general timeout fix.

Why does a click timeout when the button is visible?

An overlay, disabled state, iframe, stale element, or navigation race can prevent the action. Use a locator or inspect the element’s actionable state.

Can a longer timeout fix a browser disconnect?

No. Check Chromium compatibility, launch arguments, process limits, and protocol errors separately.