ScreenshotNeo

BlogHow-to

How to Fix Pyppeteer Timeouts After a Page Has Loaded

A page can look loaded while Pyppeteer waits on a selector, function, or navigation that never completes. Find the failing await and match its condition.

By the ScreenshotNeo team1 October 20268 min read

How to Fix Pyppeteer Timeouts After a Page Has Loaded

A page looking loaded does not mean every later Pyppeteer wait has completed. The fix is to identify the exact await that raises the timeout, then configure that operation for the state your script actually needs.

goto() waits for a navigation event, waitForSelector() waits for a DOM element (and optionally its visibility), waitForFunction() waits for a JavaScript expression to become truthy, and waitForNavigation() waits for a navigation or reload. Treating all of these as “page load” creates misleading fixes.

The API meanings below come from the Pyppeteer API reference. That reference documents 30 seconds as the default timeout for navigation, selector, and function waits, and a 500 millisecond window for network-idle conditions.

1. Find the await that actually timed out

Separate each operation and log its start and end. A successful navigation does not prove that a selector exists or that application JavaScript has reached the state your next step expects.

import asyncio
from pyppeteer import launch

async def timed(label, operation):
    print(f"START {label}")
    try:
        result = await operation
        print(f"DONE  {label}")
        return result
    except Exception as exc:
        print(f"FAIL  {label}: {type(exc).__name__}: {exc}")
        raise

async def main():
    browser = await launch(headless=True)
    page = await browser.newPage()
    try:
        await timed(
            "goto",
            page.goto("https://example.com", {"waitUntil": "load", "timeout": 30000}),
        )
        await timed(
            "selector",
            page.waitForSelector("h1", {"timeout": 30000}),
        )
    finally:
        await browser.close()

asyncio.get_event_loop().run_until_complete(main())

Keep the complete exception text. It tells you which method failed and prevents you from changing navigation settings when the real problem is a missing selector or an application condition.

2. Understand what “loaded” means

Operation Completion condition Typical reason it times out
goto() A selected navigation lifecycle event The chosen event never occurs, or the response is genuinely slow
waitForSelector() A selector matches the live DOM; with visible: true, it must also be visible Wrong selector, wrong frame, delayed rendering, or hidden element
waitForFunction() The page function returns a truthy value The condition is impossible or checks the wrong state
waitForNavigation() A navigation or reload occurs The click only changes application state, or the wait was armed too late

If a selector already exists when waitForSelector() starts, it should resolve immediately. A page can therefore be visually complete while a later wait still correctly reports that its own condition has not happened.

Each Pyppeteer wait has its own completion condition, even after navigation succeeds.
Each Pyppeteer wait has its own completion condition, even after navigation succeeds.

3. Diagnose in a repeatable order

  1. Record the Pyppeteer version, Python version, browser executable and browser version.
  2. Log every awaited operation separately.
  3. Copy the full timeout message, including the method name and selector or condition.
  4. Check the live page and frame where the operation runs.
  5. Choose the narrowest condition that matches the next action.
  6. Set the timeout on the failing operation, then retest.

Do not assume a Puppeteer bug report applies to Pyppeteer. Puppeteer and Pyppeteer are different projects, and a regression reported for Puppeteer 19.8.0 does not establish the same defect in your installed Pyppeteer version.

4. Fix a timeout from page.goto()

Pyppeteer documents four waitUntil values:

  • load: wait for the page’s load event. This is the default.
  • domcontentloaded: wait until the document has been parsed and the DOMContentLoaded event fires.
  • networkidle0: require no more than zero active network connections for at least 500 ms.
  • networkidle2: require no more than two active network connections for at least 500 ms.

Use the event required by the next step. If the script only needs the parsed document, domcontentloaded may be appropriate. If it needs a client-rendered result, use a navigation event that can finish and then wait explicitly for the result.

await page.goto(
    url,
    {
        "waitUntil": "domcontentloaded",
        "timeout": 60000,
    },
)
await page.waitForSelector("#results", {"timeout": 30000})

A site that continuously polls, opens analytics connections, or streams data may never reach an idle condition. Do not switch to a weaker condition unless the content required by your script is ready at that point. The per-call timeout controls this navigation; 0 disables the method timeout and should only be used when an intentionally unbounded wait is acceptable.

page.setDefaultNavigationTimeout(60000)
await page.goto(url, {"waitUntil": "load"})

5. Fix a timeout from waitForSelector()

Check these causes in order:

  • The selector does not match the current DOM. Inspect the exact spelling, escaping and nesting.
  • The element is inside an iframe. Query the correct frame instead of the top-level page.
  • The application has not created the element yet. Wait for the state that creates it.
  • The element exists but is hidden. With visible: true, Pyppeteer requires it to be in the DOM and not have display: none or visibility: hidden.
await page.goto(url, {"waitUntil": "domcontentloaded", "timeout": 60000})
await page.waitForSelector("#results", {"timeout": 30000, "visible": True})

In Python, use True; in JavaScript examples use true. If the page uses an iframe:

frame = next((f for f in page.frames if f.url.startswith("https://app.example.com")), None)
if frame is None:
    raise RuntimeError("Target frame was not found")
await frame.waitForSelector("#results", {"timeout": 30000})

The official reference summarizes the failure this way: “If the selector doesn’t appear after the timeout milliseconds of waiting, the function will raise error.”

6. Fix a timeout from waitForFunction()

waitForFunction() is not a general “wait until loaded” call. It resolves only when the supplied page function returns a truthy value. Verify that the expression can become true on this page and that it is checking the right global or DOM state.

await page.waitForFunction(
    "() => window.appState && window.appState.ready === true",
    {"timeout": 30000, "polling": "raf"},
)

The documented polling choices are raf (the default), mutation, or a numeric interval in milliseconds. Choose mutation when DOM changes drive the state, or a numeric interval when checking on a fixed cadence is sufficient.

7. Fix a timeout from waitForNavigation()

First confirm that the action really navigates or reloads. History API URL changes count as navigation, while a hash-only change can return None. A click that only updates application state may never satisfy a navigation wait.

Arm the wait before triggering the action. This prevents the navigation event from being missed:

navigation = asyncio.ensure_future(page.waitForNavigation({"timeout": 30000}))
await page.click("a.next")
await navigation

If the click updates a result panel without navigation, wait for that panel instead:

await page.click("button.load-more")
await page.waitForSelector(".result-card:nth-child(11)", {"timeout": 30000})

8. Complete Pyppeteer diagnostic script

This script uses a shorter navigation condition, then waits for the content the task needs. Replace the URL and selector with values from your page.

import asyncio
from pyppeteer import launch

URL = "https://example.com"
RESULT_SELECTOR = "h1"

async def main():
    browser = await launch(headless=True)
    page = await browser.newPage()
    page.setDefaultNavigationTimeout(60000)
    try:
        print("Navigating")
        response = await page.goto(
            URL,
            {
                "waitUntil": "domcontentloaded",
                "timeout": 60000,
            },
        )
        if response is None:
            raise RuntimeError("Navigation returned no response")

        print("Waiting for required content")
        await page.waitForSelector(RESULT_SELECTOR, {"timeout": 30000})
        print("Ready:", await page.title())
    finally:
        await browser.close()

if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())

9. Or skip the browser setup

If your goal is a clean website screenshot rather than browser automation, ScreenshotNeo returns a PNG, JPEG, WebP or PDF from one GET request. Its cleanup steps accept cookie and consent banners before capture and remove more than 60 known consent platforms, newsletter popups and chat widgets. Each step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing result.

ScreenshotNeo cleans common consent banners, popups and chat widgets before capture.
ScreenshotNeo cleans common consent banners, popups and chat widgets before capture.

See the ScreenshotNeo API documentation for all options.

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 fs = require('node:fs/promises');

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(`ScreenshotNeo returned ${res.status}`);
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also provides full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, PDF paper settings and page ranges, custom CSS and JavaScript, pre-capture clicks, selector or network-idle waits, request and resource blocking, custom headers and cookies, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API and an OpenAPI specification.

An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients. Every plan includes every feature. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account with 1,000 screenshots per month and no card.

10. Troubleshooting checklist

Symptom Likely cause Fix
goto() times out on a page that appears in a browser The selected lifecycle event never completes, often because of long-lived connections Try domcontentloaded, then wait for the required selector or function explicitly.
waitForSelector() never returns Wrong selector, delayed rendering, wrong frame, or hidden element Inspect the live DOM, select the intended frame, remove visible: true if visibility is not required, or wait for the state that creates the element.
waitForFunction() times out The expression never becomes truthy Evaluate the expression in the page, verify variable names and choose raf, mutation or an interval that matches the update mechanism.
waitForNavigation() times out after a click The click changes state without navigation, or the wait started after the click Start the wait before clicking; if there is no navigation, wait for the changed DOM instead.
Raising the timeout changes nothing The condition is impossible, not merely slow Validate the selector, frame, function result and navigation behavior before increasing the limit.
Timeout appears only in one environment Different Pyppeteer, Python, browser or executable versions Record all versions and the browser path, then reproduce with the same runtime context.

11. Performance, reliability and cost

  • Performance: Use the earliest navigation event that still guarantees the next operation is safe. Waiting for network idle on a page with polling can add unnecessary delay or never finish.
  • Reliability: Prefer a specific selector or application-ready condition over a fixed sleep. Use a timeout long enough for the site’s normal behavior, but keep the failure bounded.
  • Concurrency: Do not create unbounded browser pages or waits. Close pages and browsers in finally blocks and keep navigation and selector waits separate so failures are diagnosable.
  • Cost: Self-hosted Pyppeteer consumes your own compute and browser maintenance time. ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads and cache hits cost nothing, with the result indicated by X-Page-Verdict and X-Billed headers.

12. FAQ

Does a successful goto() mean the page is ready?

No. It means the selected navigation condition completed. A client-rendered component may still need a selector or function wait.

Should I always use networkidle0?

No. Polling, analytics and streaming connections can prevent it from completing. Select the condition that matches the content your next step needs.

Is timeout: 0 a permanent fix?

No. It disables that method’s timeout and can leave a worker waiting forever when the condition is impossible.

A hash-only URL change can return None from waitForNavigation(). Wait for the DOM or application state that the hash change triggers.

Can I use ScreenshotNeo for PDFs as well as images?

Yes. Its API supports PDF output with paper size, margins, landscape mode and page ranges.