How to Set Reliable Timeouts in Pyppeteer
Set bounded Pyppeteer navigation, selector, function, request, and response timeouts—and diagnose failures caused by the wrong wait condition.
Use page.setDefaultNavigationTimeout(timeout_ms) for a page-wide navigation limit, then set explicit timeout values on selector, function, request, and response waits. Pyppeteer timeout values are milliseconds. The documented default for these operations is 30 seconds; passing 0 disables the timeout, which should be deliberate.
A navigation timeout only governs navigation methods such as goto(), goBack(), goForward(), reload(), and waitForNavigation(). It does not automatically limit every other wait in your script.
Set a navigation timeout
The following complete example sets a 60-second default for navigation and waits only until the initial HTML document has been parsed:
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=True)
page = await browser.newPage()
# The value is milliseconds.
page.setDefaultNavigationTimeout(60_000)
try:
response = await page.goto(
"https://example.com",
{
"waitUntil": "domcontentloaded",
"timeout": 60_000,
},
)
print("status:", response.status if response else "no response")
print("title:", await page.title())
finally:
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
setDefaultNavigationTimeout() changes the default used by navigation operations. A per-call timeout overrides that default for one navigation. See the Pyppeteer API reference for the version installed in your project.
Choose the right completion condition
goto() can resolve on different lifecycle conditions through waitUntil:
| Condition | What it waits for | Use it when |
|---|---|---|
load |
The load event fires. | You need the page’s normal load lifecycle to complete. |
domcontentloaded |
The initial document is parsed without waiting for every resource. | You need the DOM quickly and can wait for application state separately. |
networkidle0 |
No more than zero active network connections for at least 500 ms. | The page should become completely quiet. |
networkidle2 |
No more than two active network connections for at least 500 ms. | The page may keep a small amount of background traffic. |
Pages with analytics, polling, websockets, advertisements, or streaming data may never reach a network-idle condition promptly. If the task needs a later application state, use a bounded selector or predicate wait instead of repeatedly increasing the navigation timeout.
# Fast document parse, then wait for the application shell.
await page.goto(
"https://example.com/app",
{"waitUntil": "domcontentloaded", "timeout": 30_000},
)
await page.waitForSelector(
"main[data-ready='true']",
{"timeout": 20_000},
)
Timeouts for selectors, functions, requests, and responses
These waits have their own timeout options. Configure the operation you are actually waiting for:
# Selector wait: milliseconds.
await page.waitForSelector("#results", {"timeout": 15_000})
# Predicate wait: milliseconds.
await page.waitForFunction(
"() => document.querySelectorAll('.item').length > 0",
{"timeout": 15_000},
)
# Network request and response waits also accept timeout options.
await page.waitForRequest(
lambda request: "/api/data" in request.url,
{"timeout": 10_000},
)
await page.waitForResponse(
lambda response: response.url.endswith("/api/data"),
{"timeout": 10_000},
)
The documented default for these waits is also 30 seconds, and 0 disables the individual timeout. A disabled wait can remain pending indefinitely when a selector or request never appears.
Per-navigation overrides
Keep a finite page default and override exceptional navigations explicitly:
page.setDefaultNavigationTimeout(30_000)
# This one page is known to be slower.
await page.goto(
"https://example.com/report",
{
"waitUntil": "load",
"timeout": 90_000,
},
)
This makes the exceptional case visible in code and prevents a slow URL from silently changing every later navigation.
Handling timeout failures
- Record the URL, operation, configured timeout, and
waitUntilvalue. - Determine whether the failure occurred in navigation or in a later selector, function, request, or response wait.
- Try a less demanding completion condition, such as
domcontentloaded, when the page continues background requests. - Wait for the specific element or predicate that represents readiness, with its own finite timeout.
- Capture a screenshot, page content, console output, and failed requests before closing the browser.
- Increase the bound only when the page genuinely needs more time; do not use a larger number to hide an incorrect readiness condition.
from pyppeteer.errors import TimeoutError
try:
await page.goto(
url,
{"waitUntil": "domcontentloaded", "timeout": 45_000},
)
await page.waitForSelector(
"#report-ready",
{"timeout": 20_000},
)
except TimeoutError as exc:
print(f"timed out while loading {url}: {exc}")
await page.screenshot({"path": "timeout-debug.png", "fullPage": True})
print((await page.content())[:2000])
raise
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Navigation Timeout Exceeded |
The selected lifecycle event was not reached before the navigation timeout. | Check waitUntil, keep a finite timeout, and use a readiness selector when appropriate. |
Timeout occurs at waitForSelector() |
The selector is wrong, inside a frame, rendered only after an action, or never appears. | Verify the selector in the target page, inspect frames, perform the required action, and set a selector-specific timeout. |
networkidle0 never completes |
Polling, analytics, websockets, ads, or another long-lived request keeps the network busy. | Use domcontentloaded or load, then wait for the element that proves the page is ready. |
| Timeout after increasing navigation time | The later wait has its own 30-second timeout. | Set timeout on waitForSelector, waitForFunction, waitForRequest, or waitForResponse. |
| The script hangs indefinitely | A timeout was set to 0. |
Restore a finite bound unless an unbounded operation is intentional and externally supervised. |
| Different behavior across machines | Network speed, CPU, Chromium revision, page content, and Pyppeteer version differ. | Pin the environment where practical, log timings, and validate timeout choices against the installed version. |
Reliability and performance checklist
- Use milliseconds consistently and name timeout constants clearly.
- Set a page-wide navigation default once, then use per-operation overrides for special cases.
- Prefer the narrowest readiness signal that represents success.
- Keep navigation and post-navigation waits separate so failures identify the failed phase.
- Use finite bounds and catch timeout exceptions at the job boundary.
- Log elapsed time, URL, lifecycle condition, and selector or request being awaited.
- Close the browser in a
finallyblock so a timeout does not leak Chromium processes. - Do not assume a single duration is reliable for every website or workload; the Pyppeteer documentation defines semantics and defaults, not a universal recommendation.
Or skip the browser setup
If your goal is a clean image or PDF rather than browser automation, ScreenshotNeo provides a single HTTP request. Its capture pipeline accepts cookie and consent banners before the shot, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you turn each cleanup step off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots; the response identifies the result with X-Page-Verdict and X-Billed headers.
Read the full option list in 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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);
ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element shots, custom wait conditions, request blocking, headers, cookies, user agents, timezone and geolocation, custom JavaScript and CSS, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, PDFs, HTML/CSS rendering, and an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents. Every feature is on every plan: 1,000 shots per month are free with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and get 1,000 screenshots each month with no card.
FAQ
What unit does Pyppeteer use for timeouts?
Milliseconds. For example, 30_000 means 30 seconds.
Does setDefaultNavigationTimeout() affect selector waits?
No. It sets the default for navigation methods. Selector, function, request, and response waits have their own timeout options.
Should I always use networkidle0?
No. It is appropriate only when zero active connections for at least 500 ms represents readiness. Polling and streaming pages may never satisfy it.
What does timeout 0 mean?
For the documented operations, 0 disables the timeout. Use it only with an external cancellation or supervision strategy.
Can one navigation have a different timeout?
Yes. Pass a timeout option to that goto(), reload(), history navigation, or navigation wait.


