Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

page.goto() usually is not “randomly hanging.” It is waiting for a navigation condition, a request that interception left unresolved, a server or TLS connection that never completes, or a separate application-ready signal that never appears. The fastest fix is to identify exactly which promise is pending before changing the timeout.

Start with a bounded navigation, capture the URL, response, failures, and page errors, then check the waitUntil condition, interception handlers, and event ordering. Only increase a timeout after you know the page is slow but finite.

What page.goto() is waiting for

Puppeteer navigates a frame to a URL and resolves with the main resource response. If redirects occur, the response for the final redirect target is returned. A same-document hash change or an about:blank navigation can resolve with null, because no new document response was required.

A navigation can fail rather than resolve when the URL is invalid, the server is unreachable or nonresponsive, TLS validation fails, the main resource fails, a timeout expires, or an allowlist/blocklist restriction prevents the request. A long wait is therefore a symptom, not a diagnosis.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Separate navigation from readiness

Document navigation completion does not guarantee that your application has rendered the data your script needs. A page can finish loading its document while an API request, client-side route, or component is still pending. Conversely, a page can keep background connections open after the content you need is already visible.

Use page.goto() for the document lifecycle, then wait for a task-specific selector or response. page.waitForSelector() has its own timeout and throws if the element never appears, which helps distinguish a successful navigation from missing application content.

First, capture what is actually pending

Before changing settings, record the exact URL, Puppeteer and browser versions, the complete error text, elapsed time, and whether the promise remains pending or eventually throws. This small diagnostic wrapper records the main response and the browser-side symptoms that commonly explain a perceived hang.

const puppeteer = require('puppeteer');

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

  page.on('console', message => {
    console.log(`[console:${message.type()}] ${message.text()}`);
  });
  page.on('pageerror', error => {
    console.error('[pageerror]', error);
  });
  page.on('requestfailed', request => {
    console.error('[requestfailed]', request.url(), request.failure());
  });
  page.on('response', response => {
    if (response.request().isNavigationRequest() &&
        response.request().frame() === page.mainFrame()) {
      console.log('[main response]', response.status(), response.url());
    }
  });

  const started = Date.now();
  try {
    const response = await page.goto(url, {
      waitUntil: 'domcontentloaded',
      timeout: 30_000,
    });
    console.log('navigation result:', response
      ? {status: response.status(), url: response.url()}
      : null);
    console.log('elapsed_ms:', Date.now() - started);
  } catch (error) {
    console.error('goto failed after ms:', Date.now() - started);
    console.error(error);
    console.error('current URL:', page.url());
  } finally {
    await browser.close();
  }
})();

This does not make the navigation faster; it tells you whether the document response arrived, which URL is current after redirects, and whether failed requests or page exceptions occurred. Keep the version information with these logs when asking for help.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Check the URL and transport before tuning Puppeteer

Validate the URL

Pass an absolute URL with a supported scheme, normally https:// or http://. Log the exact string after any configuration or template substitution. A missing scheme, malformed escape, or accidental whitespace can produce an invalid-navigation error that looks like a timeout in higher-level code.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Check DNS, connectivity, and server response

Try the same URL from the machine running Chromium, not only from your laptop. A private hostname, container DNS configuration, firewall, proxy, or outbound policy can prevent the browser from reaching the host. If the server accepts a connection but never sends a document, extending Puppeteer’s timeout only delays the inevitable.

Check TLS and access controls

Certificate errors, a server-side bot check, an IP restriction, or an allowlist/blocklist rule can stop the main resource. The diagnostic listeners above help reveal request failures, but inspect the full thrown error as well; it often identifies a certificate or timeout category. Do not treat a successful TCP connection as proof that a document navigation can complete.

Choose a navigation lifecycle that matches the job

The waitUntil option controls which lifecycle milestone must be reached before goto() resolves. Pick the earliest milestone that supports your task, then wait for the application signal you actually need.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Condition What it represents When it fits Typical reason it appears to hang
domcontentloaded The initial document has been parsed. Scraping markup that is present in the initial HTML, or handing readiness to a selector wait. Usually not blocked by images, analytics, or most post-load requests; the required app content may still be absent.
load The document’s load lifecycle has completed. Work that depends on resources participating in the page load event. A slow image, script, font, or other load-blocking resource delays completion.
Network-idle condition No more than the configured number of concurrent connections for the documented idle interval; Puppeteer documents a 500 ms idle interval and a default of zero concurrent connections for the strict condition. Pages that genuinely become quiet and where network silence is a useful proxy. Polling, analytics, streaming, advertisements, service workers, or long-lived connections keep traffic above the threshold.

Network-idle is a specific condition, not a universal definition of “ready.” A page can be usable while background traffic continues, or it can become network-idle before a client-side component has rendered the data you need. A more reliable pattern is to use a narrower lifecycle and an explicit readiness selector:

await page.goto(url, {waitUntil: 'domcontentloaded'});
await page.waitForSelector('[data-ready="true"]', {timeout: 15_000});

The selector in this example is illustrative. Use a stable element or an expected response that belongs to your target application.

Check the timeout that actually applies

Puppeteer’s navigation and wait operations have configurable timeouts. The documentation for the 25.12.0 version line lists 30,000 milliseconds as the generic default, while 0 disables a timeout. A per-call option takes precedence over a page or browser default, so inspect all three scopes in your code.

Set a bounded timeout deliberately

page.setDefaultNavigationTimeout(45_000);
page.setDefaultTimeout(15_000);

await page.goto(url, {
  waitUntil: 'domcontentloaded',
  timeout: 45_000,
});

A longer bound is reasonable for a known slow but finite origin. It does not resolve an intercepted request, a never-ending network-idle condition, or a server that never responds. Avoid using timeout: 0 as a general fix: an outage or logic bug can then leave a worker occupied indefinitely.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Identify which wait timed out

Log around every await, not only around goto(). If navigation succeeds and waitForSelector() later times out, the document was not the problem. If the log never reaches the line after goto(), inspect transport, lifecycle selection, interception, and the navigation timeout.

Audit request interception

When request interception is enabled, requests stall until your code continues, answers, aborts, or completes them through the browser cache. The Puppeteer Page API states: “Once request interception is enabled, every request will stall unless it’s continued, responded to or completed using the browser cache.” One forgotten branch is enough to make the main document appear frozen.

Resolve every branch

await page.setRequestInterception(true);

page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;

  if (shouldBlock(request)) {
    return request.abort();
  }

  return request.continue();
});

shouldBlock must be your own function. In production, handle exceptions and make sure no other listener tries to resolve the same request. A handler that returns without calling one of the resolution methods leaves that request pending.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

Remember indirect interception

Authentication support can enable interception behind the scenes according to the Page API. If a navigation began hanging after adding authentication or another helper that configures requests, include that helper in the audit. Temporarily disable interception as a diagnostic; if the navigation immediately works, restore the feature and fix the unresolved branch rather than increasing the timeout.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fix click-and-navigation races

If a click triggers a full document navigation, register the navigation wait before triggering the click and await both promises together. Registering the wait afterward can miss the navigation event, leaving the code waiting for an event that already occurred.

const [response] = await Promise.all([
  page.waitForNavigation({waitUntil: 'domcontentloaded'}),
  page.click('a.next'),
]);

if (response) {
  console.log('new document:', response.status(), response.url());
} else {
  console.log('same-document navigation or no document response');
}

This pattern is for document navigation. A single-page application may change its route or hash without fetching a new document, in which case waitForNavigation() can resolve with null. Wait for the route-specific selector, URL change, or API response that proves the SPA transition completed.

Separate HTTP success from useful content

A resolved response is not automatically a successful application result. Inspect the status when a document response exists, and then verify the content your task requires.

const response = await page.goto(url, {waitUntil: 'domcontentloaded'});

if (response && !response.ok()) {
  throw new Error(`Document returned HTTP ${response.status()}`);
}

await page.waitForSelector('#results', {timeout: 20_000});

This catches a server-rendered error page or authorization response that loaded normally but cannot satisfy your scraper or test. For data that arrives through XHR or fetch, wait for the specific response or for a DOM state that is created only after the data is processed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A practical troubleshooting sequence

  1. Capture the facts. Record the URL, Puppeteer and browser versions, full error text, elapsed time, current URL, console messages, page errors, failed requests, and the main document response.
  2. Check transport. Validate the URL, then test DNS, connectivity, proxy rules, TLS, access restrictions, and whether the server actually returns a document from the runner.
  3. Inspect waitUntil. Replace an unnecessarily strict network-idle wait with domcontentloaded or load, then add a selector or response wait tied to the task.
  4. Check timeout scope. Find per-call values, page defaults, and browser defaults. Increase the bound only when the navigation is demonstrably slow but finite.
  5. Audit interception. If interception is enabled directly or indirectly, ensure every request is continued, responded to, aborted, or served from cache exactly once.
  6. Fix event ordering. For action-triggered navigation, put waitForNavigation() before the click in Promise.all(). For SPA or hash changes, use an application-specific readiness signal.
  7. Verify the result. Check the response status where available and wait for the selector, response, or state that proves the page is usable.

Reliability and performance practices

Use bounded work per page

Keep navigation, selector, and task-specific waits bounded independently. This prevents one broken origin from consuming a worker forever and makes your logs show which stage failed. If you retry, retry only classified transient failures and preserve the original error and attempt count; retries cannot repair a deterministic selector mistake or an unresolved interception branch.

Prefer the smallest sufficient lifecycle

Waiting for every network connection to disappear can add latency and reduce throughput on modern sites. A fast document milestone followed by a precise selector or response is usually both quicker and closer to the actual requirement. Do not remove waits merely to hide failures: replace vague readiness with a signal you can explain.

Keep a minimal reproduction

Reduce the case to one URL, one page, one navigation option, and no application-specific handlers. Then add interception, authentication, custom headers, and post-navigation waits back one at a time. If the documented checks do not isolate the issue, share that minimal reproduction together with versions and logs rather than describing every long wait as a browser hang.

Or skip the browser setup

If your goal is a clean image or PDF rather than interactive browser automation, ScreenshotNeo provides a website screenshot API and MCP server. It handles the capture in one request, so your code does not need to launch Chromium, coordinate navigation events, or maintain interception handlers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API documentation at https://screenshotneo.com/docs/ for parameters and response headers. A basic cURL request is:

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

The same request in 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)

And in 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}`);

Before capture, ScreenshotNeo accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

There is a free allowance of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to try it without a card.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.