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

There is no single Puppeteer signal that proves every page is fully ready. By default, page.goto() waits for the browser’s load event. That is enough when you need the document and its load-event subresources, but a single-page application may still be fetching data or rendering afterward. Choose a lifecycle event for the document, then—when the page’s content matters—wait for a stable selector or application-ready condition.

Choose what “finished loading” means for your task

A page can be ready in one sense but not another. Its HTML may be parsed while images are still loading; the browser may fire load before an application has rendered API results; and the network may become quiet even though the interface is not in the state your test needs. Select the boundary that matches the work you are doing.

Goal Wait condition What it establishes Trade-off
Read the initial document structure domcontentloaded The DOMContentLoaded event was dispatched. Data, images, and other work that happens later may be absent.
Wait for the browser’s ordinary page-load boundary load The browser load event was dispatched. This is the default for page.goto(). Client-side rendering or later API work can continue.
Require no active network connections for a quiet period networkidle0 No more than zero active connections for at least 500 ms. Polling, long requests, sockets, or analytics can prevent it from completing.
Tolerate a small amount of background network activity networkidle2 No more than two active connections for at least 500 ms. Two connections can remain while the interface you need is still incomplete.
Confirm that a particular interface element is ready waitForSelector() or waitForFunction() A specified element or application condition is present. You must choose a stable, meaningful readiness condition.

The two network-idle thresholds describe network activity, not user-visible completeness. For a data-driven page, the most useful final check is usually the element or state that proves the result you care about is available.

Use Puppeteer’s default load boundary

page.goto(url) navigates to the URL and resolves with the main resource’s response. Unless you set waitUntil, Puppeteer uses load. A minimal CommonJS script using that default looks like this:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    const response = await page.goto('https://example.com');

    console.log('Navigation complete');
    console.log('HTTP status:', response ? response.status() : 'no main response');
    console.log('Title:', await page.title());
  } finally {
    await browser.close();
  }
})();

The response may be null for cases such as about:blank or a hash-only navigation, so do not call status() without checking it. Also, a resolved navigation does not necessarily mean the server returned a successful status: inspect the response status when that distinction matters. Valid HTTP error statuses such as 404 or 500 do not necessarily make goto() throw.

Set waitUntil for a specific lifecycle event

Use waitUntil to state the document-level boundary explicitly. It accepts one lifecycle event or an array; with an array, navigation is considered successful after all listed events have fired.

await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.goto(url, { waitUntil: 'load' });
await page.goto(url, { waitUntil: 'networkidle0' });
await page.goto(url, { waitUntil: 'networkidle2' });

For example, waiting for both DOM parsing and a modest period of network quiet can be useful when the page has a little background traffic:

const response = await page.goto('https://example.com', {
  waitUntil: ['domcontentloaded', 'networkidle2'],
  timeout: 60000,
});

Navigation’s documented default timeout is 30,000 ms. Set a different timeout when you have a reason to allow more or less time; a larger timeout only gives a slow condition longer to complete. It does not make an unsuitable condition more accurate.

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

Wait for the content an application actually renders

For a single-page application, first wait for the document to be parsed, then wait for a visible, user-meaningful element that represents readiness. This separates the browser’s document lifecycle from the app’s rendering lifecycle.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    const response = await page.goto('https://example.com/results', {
      waitUntil: 'domcontentloaded',
      timeout: 30000,
    });

    await page.waitForSelector('[data-testid="results"]', {
      visible: true,
      timeout: 30000,
    });

    console.log('Results are visible');
    console.log('HTTP status:', response ? response.status() : 'no main response');
  } finally {
    await browser.close();
  }
})();

waitForSelector() resolves when the selector appears in the DOM. With visible: true, it also requires the element to be visible; if the condition is not met before the timeout, the wait throws. Choose a selector that signals the data or interface state your task needs—not a generic wrapper that appears before the useful content.

When the application exposes a readiness flag instead of a suitable element, wait for that condition with waitForFunction():

await page.waitForFunction(() => window.appReady === true, {
  timeout: 30000,
});

The predicate should reflect a real application state. A condition that is true too early, or that never becomes true on an error path, can make the automation either proceed prematurely or time out. If the page may fail to load its data, consider making the predicate account for the app’s explicit success and failure states.

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

Use network-idle waits only when network quiet is the goal

networkidle0 and networkidle2 can help when a quiet network is itself useful, but neither proves that a particular UI is ready. A page can finish its requests and render afterward; conversely, a page can remain functionally usable while periodic or persistent traffic prevents a strict idle condition.

You can also wait for network idle separately from navigation and specify an idle period:

await page.goto(url, { waitUntil: 'networkidle2' });
await page.waitForNetworkIdle({ idleTime: 1000 });

waitForNetworkIdle() resolves once the network is idle and waits at least the configured idle time. Use it deliberately: an idle wait can extend the total time spent on a page, and increasing the quiet period still cannot establish that the right content has appeared. When content is the goal, follow a suitable navigation boundary with a selector or predicate instead.

Listen for lifecycle events when you need diagnostics

Event listeners are useful for logging the browser’s progress, especially while investigating a timing problem:

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.
page.once('domcontentloaded', () => console.log('DOM parsed'));
page.once('load', () => console.log('Browser load fired'));

await page.goto(url);

Register listeners before navigation if you want to observe events that occur during it. These events report that the corresponding browser lifecycle event was dispatched; they do not tell you that framework data binding, an application request, or a later render is complete.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot waits that are early or never finish

load fires, but the content you need is missing

The page may render its interface after the browser load event. Keep an appropriate navigation boundary and add a visible selector or application predicate for the specific content. Do not replace it with a longer generic timeout and assume that the content will be ready at the end.

networkidle0 times out

Persistent requests, polling, service workers, tracking activity, sockets, or long-running requests can prevent the connection count from reaching zero. If zero connections is not essential, use a more suitable lifecycle boundary and wait for the page’s actual ready signal. Consider networkidle2 only when allowing up to two connections matches the page and task.

networkidle2 finishes before the app is ready

That condition allows up to two active connections, and a quiet interval still says nothing about whether the target interface has rendered. Add a selector or predicate after navigation to verify the application state you need.

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

A selector wait times out

  • Check that the selector matches the current page’s markup and is not changed between runs.
  • Check whether the element exists but is hidden; if so, decide whether visibility is required and whether the app will reveal it.
  • Confirm that the expected authentication state is present and that the page did not render an error or empty state instead.
  • Check whether the element is inside an iframe or shadow root. A page-level selector may not reach content in another frame or a shadow tree; use the appropriate frame or shadow-root handling for that page.
  • Use an application-specific success or failure condition when a missing element could indicate a real application error rather than slow loading.

goto() resolves but the navigation was not successful

Inspect the returned response’s HTTP status rather than treating a resolved promise as proof of a successful page. Some error statuses still produce a valid response and do not cause goto() to throw.

Or skip the browser setup

If your goal is a screenshot or PDF rather than asserting the readiness state inside a Puppeteer test, ScreenshotNeo offers a one-request capture API. It is not a substitute for a test that must verify a particular application condition.

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

See the ScreenshotNeo API documentation for request options. ScreenshotNeo accepts cookie and consent banners before capture 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 responses identify the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 shots a month without a card; paid plans start at $5 for 3,000 shots.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card required.

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

Frequently Asked Questions

What does a null response from Puppeteer’s page.goto() mean?

Some navigations, including about:blank and hash-only changes, may not have a main-resource response. Check for null before reading the response status.

Can Puppeteer tell whether an element is inside an iframe?

A page-level selector may not reach iframe content. Identify the relevant frame and perform the selector wait in that frame’s context.

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.