· Troubleshooting · By Simon Chiu

Puppeteer: Navigation Timeout of 30000 ms Exceeded

Navigation timeout of 30000 ms exceeded in Puppeteer has four common causes. We reproduced three on Puppeteer 25 and show a tested fix for all four.

TimeoutError: Navigation timeout of 30000 ms exceeded means page.goto() did not see the event you asked for (waitUntil, default load) within 30 seconds. The server may have answered long ago. The page can even look finished. Puppeteer only knows that one specific signal did not arrive. The fix is almost never a bigger number. It is a different signal: return at domcontentloaded, then wait for the element you actually need.

This page is part of our Puppeteer screenshot guide. It covers four common causes, reproduces the first three with timings from a real run, and gives one tested script that handles all four. The fourth, an honestly slow page, needs no test page: it is the case where a longer timeout is correct.

Tested on 2026-10-18 with Node 22.18.0, [email protected] (Chrome 155.0.8059.79), macOS on Apple silicon. Test pages were served by a local Node HTTP server.

The reproduction

We built a small HTTP server with one route per failure mode, then called page.goto() against each route with different waitUntil values. Every row below is one real attempt. The timeout was 30 s for the first row and 5 s for the rest, so the run took one minute instead of three.

Route What the server does waitUntil Result
/hang Accepts the connection, never sends a response load (default) TimeoutError after 30,003 ms
/slow-image Returns HTML at once; one <img> request never finishes load TimeoutError after 5,002 ms
/slow-image same domcontentloaded OK in 35 ms
/long-poll Returns HTML; a fetch() stays open forever networkidle0 TimeoutError after 5,002 ms
/long-poll same networkidle2 OK in 957 ms
/long-poll same load OK in 27 ms
/slow-image Same page, but the hanging image URL is aborted by request interception load OK in 46 ms
10.255.255.1 Unroutable address, the TCP connect never completes load TimeoutError after 5,003 ms

The error text was identical in every failing row apart from the number. That is the main trap: the message tells you that the wait failed, not which of these four situations you are in.

Cause 1: the server never answers

/hang and the unroutable address fail before any HTML arrives. No waitUntil value helps, because none of them can fire without a document. To confirm it, run curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' <url> from the same machine. If curl also hangs, the problem is the network path: a firewall that drops your server's IP range, a bot filter that tarpits headless traffic, or a DNS record that points nowhere.

Fix: keep a short timeout so the job fails fast, and treat it as a target problem, not a Puppeteer problem. page.setDefaultNavigationTimeout(2000) made the /hang case fail in 2,002 ms in our run instead of 30 s.

Cause 2: one sub-resource never finishes

This is the most common case on real sites. The load event waits for every image, stylesheet, script and iframe in the initial document. One slow ad pixel or font on a broken CDN holds it back. In the test the HTML and heading were ready within milliseconds; only the image was missing.

Two fixes worked:

  • Return earlier with waitUntil: 'domcontentloaded' (35 ms), then wait for the element that matters.
  • Abort the request you do not need. With interception aborting the hanging URL, even the default load wait finished in 46 ms.

Cause 3: networkidle0 on a page that never goes quiet

networkidle0 waits for zero open connections for 500 ms. Chat widgets, analytics beacons, server-sent events and long-poll endpoints keep one connection open for the life of the page, so the condition can never be true. networkidle2 tolerates two open connections and finished in 957 ms on the same page; most of that time is the 500 ms quiet window.

Network idle is a guess about readiness. If you know what "ready" looks like, wait for that instead. In our /ready-but-poll route the content appeared after 800 ms while a fetch stayed open; domcontentloaded plus waitForSelector('#title') finished in 829 ms. The wait strategies guide compares the signals in more depth.

Cause 4: the timeout is too short for honest work

Sometimes the page is just slow: a cold server render, a large HTML document, a redirect chain through a login. Here a longer timeout is the right fix, but raise it for that navigation only, and keep the total job time bounded. A worker that allows 90 s per page can serve far fewer pages per minute than you planned.

The fix that covers all four

This is the script we ran on the date above. It fails fast when the server is dead, does not depend on load or network idle, drops known third-party hosts, and exits non-zero on failure so a job runner sees it.

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const readySelector = process.argv[3] ?? 'p';
const blocked = ['google-analytics.com', 'googletagmanager.com', 'doubleclick.net'];

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  page.setDefaultNavigationTimeout(15_000);
  await page.setRequestInterception(true);
  page.on('request', (req) => {
    if (blocked.some((host) => req.url().includes(host))) req.abort();
    else req.continue();
  });

  // Return as soon as the HTML is parsed, then wait for what you need.
  await page.goto(url, { waitUntil: 'domcontentloaded' });
  await page.waitForSelector(readySelector, { visible: true, timeout: 10_000 });
  await page.screenshot({ path: 'page.png' });
  console.log('saved page.png');
} catch (err) {
  if (err.name === 'TimeoutError') console.error(`Timed out: ${err.message}`);
  else console.error(err);
  process.exitCode = 1;
} finally {
  await browser.close();
}

Run it with node capture.mjs https://your-site.example '.main-content'. One note from the test run: example.com no longer has an <h1>, so a script that waits for h1 there now times out on waitForSelector. Check your ready selector against the live page, not your memory of it.

Symptom, cause, fix

Symptom Likely cause Fix
Timeout, curl also hangs Server or network never answers Short timeout, check firewall and bot rules
Timeout on load, content visible in a headed run One hanging sub-resource domcontentloaded + waitForSelector, or abort the request
Timeout only with networkidle0 Long-poll, beacon or socket networkidle2, or wait for a selector
Timeout on first request only, later ones pass Cold server or cold cache Raise timeout for that call, warm the URL
Timeout after a redirect to a login page Auth cookie missing or expired Set cookies before goto
TimeoutError: Waiting for selector ... failed Navigation was fine; the selector is wrong Check the selector on the live page
Timeout only inside Docker or CI Different egress IP blocked or rate limited Test with curl from the same container
timeout: 0 "fixed" it, but jobs pile up Unbounded wait Restore a finite timeout and fix the wait

Version notes

Version What matters here
puppeteer 25.x (tested) Default navigation timeout 30,000 ms; waitUntil accepts load, domcontentloaded, networkidle0, networkidle2
puppeteer 22+ page.waitForTimeout() was removed. Old fixes that add a fixed sleep after goto fail with "is not a function"
All versions page.setDefaultNavigationTimeout() affects navigations; page.setDefaultTimeout() also affects waitForSelector and similar waits

We ran only the 25.x row. The other rows come from the Puppeteer changelog and API docs, and we did not re-run them for this page.

The same capture with one API call

The RenderScreenshot API exposes the same pattern as request parameters: return on a known event, then wait for a selector, inside a hard limit. When a page misses the limit, the API returns the timeout error with the same "Navigation timeout of 30000 ms exceeded" message, and the failed request uses no credit. With the Node SDK:

import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);
const image = await client.take(
  TakeOptions.url('https://example.com')
    .waitFor('domcontentloaded')
    .waitForSelector('p')
    .waitForTimeout(15)
    .blockTrackers()
);

waitForTimeout is in seconds (1 to 60), as listed in the wait parameters. We did not call the live API for this page. If your screenshots finish without a timeout but come out white, read why a Puppeteer screenshot is blank next. For memory leaks and zombie processes after timeouts, see Puppeteer timeouts and memory leaks.

Create a free API key to try the request on your own slow pages.

Related in this topic