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
loadwait 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
- Puppeteer Screenshots: Tested Recipes for Puppeteer 25 Every Puppeteer screenshot recipe re-run on Puppeteer 25.14 and Chrome 155: viewport, full page, element, clip, waits, formats, auth, blocking and fonts.
- Puppeteer on AWS Lambda with @sparticuz/chromium Run Puppeteer on AWS Lambda with @sparticuz/chromium: version matching, a container-image handler tested in the Lambda emulator, and warm reuse.
- Puppeteer in Docker: A Tested Dockerfile with Fonts A non-root Puppeteer Dockerfile with Chromium, CJK and emoji fonts and dumb-init, plus the sandbox error it hits, measured image sizes and launch times.
- Puppeteer Full Page Screenshot: Limits, Lazy Images, Fixes Puppeteer full page screenshot guide: the 16,384 px limit we measured, captureBeyondViewport, lazy-load scrolling, sticky headers and a tested script.
- Hide Cookie Banners and Chat Widgets in Puppeteer Remove cookie banners, consent iframes, shadow-DOM dialogs and chat widgets from Puppeteer screenshots: a tested module and what each method misses.
- Puppeteer HTML to PDF: page.pdf() Options Tested Generate PDFs with Puppeteer page.pdf(): paper size, margins, header and footer templates, page breaks and print CSS, with measured sizes and timings.