How to Wait Until a Page Is Ready in Puppeteer
Learn which Puppeteer readiness signal to await—navigation events, network idle, or a specific element—before capturing or interacting with a page.
Short answer: choose the readiness signal that matches your next step. Puppeteer’s page.goto() waits for the load event by default. Use domcontentloaded when you need parsed HTML sooner, a network-idle condition when temporary network quiet is useful, or a selector wait when a specific piece of page content defines “ready.” No generic event guarantees that an application has finished all of its own work.
This guide uses JavaScript with Puppeteer. Check the API against your installed Puppeteer version, especially if your project pins an older release.
1. Choose the readiness signal
| Signal | What it means | Good fit | Limitation |
|---|---|---|---|
domcontentloaded |
The document’s DOMContentLoaded event fired. | Read parsed document structure, then wait for the exact content you need. | Images, stylesheets, and application data may still be loading. |
load |
The browser’s load event fired. This is page.goto()’s default. |
The browser load event is the milestone your task requires. | It does not prove that later asynchronous rendering or application work is complete. |
networkidle0 |
No more than zero network connections for at least 500 ms. | A quiet network is a useful proxy for completion. | Persistent connections or polling can prevent the condition from being reached. |
networkidle2 |
No more than two network connections for at least 500 ms. | A small amount of background traffic is acceptable. | Network quiet still does not establish semantic or visual readiness. |
| Selector or locator | A specific element appears or an action’s preconditions are met. | Wait for the results, chart, or control your script actually needs. | Choose a selector that represents completed content, not just a loading shell. |
The networkidle0 and networkidle2 lifecycle conditions use a 500 ms idle period. The right choice is determined by the next operation: parsing the DOM, capturing the finished page, or acting on a particular control. [Puppeteer lifecycle events]
2. Wait for navigation
Pass waitUntil to page.goto() to select the navigation milestone. This example waits for the DOM to be parsed, then prints the page title.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
console.log('HTTP status:', response?.status() ?? 'no main response');
console.log('Title:', await page.title());
} finally {
await browser.close();
}
Use waitUntil: 'load' when the load event is the desired milestone. For a quiet-network proxy, use 'networkidle0' or 'networkidle2'. page.goto() resolves with the main resource response; on redirects, that is the last redirect response. Navigation to about:blank or a same-URL hash change can return null. In headless shell mode, a valid HTTP error status such as 404 or 500 does not necessarily make navigation throw, so inspect response.status() when status matters. [Puppeteer page.goto()]
3. Wait for the content or control you need
For dynamic pages, waiting for a meaningful selector is usually more reliable than guessing how long the page will take. Here the script waits for visible search results after the initial document is parsed.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com/search?q=puppeteer', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
const results = await page.waitForSelector('[data-testid="results"]', {
visible: true,
timeout: 10_000,
});
if (!results) throw new Error('Results element was not found');
try {
console.log(await results.evaluate(element => element.textContent?.trim()));
} finally {
await results.dispose();
}
} finally {
await browser.close();
}
waitForSelector() resolves when the selector appears. With visible: true, the element must be in the DOM and visible. It works across navigations, has a documented 30-second default timeout, and returns an element handle when found; dispose of that handle when finished. [Puppeteer page.waitForSelector()]
For current Puppeteer interaction code, locators are often a better fit: they wait for the element to reach the state required for the requested action, including visibility and enabled state where relevant. A selector wait alone does not retry a later click or other action. [Puppeteer page interactions]
// Locator example: wait for the control's action preconditions, then click.
await page.locator('button[type="submit"]').click();
Prefer an application-specific selector such as a results container or a completed-state marker. Waiting for a generic container can return while it is still empty or showing a spinner.
4. Wait for network idle when it fits
You can use a navigation lifecycle condition or explicitly wait for network idle after navigation:
await page.goto('https://example.com/report', {
waitUntil: 'domcontentloaded',
});
await page.waitForNetworkIdle({
concurrency: 2,
idleTime: 500,
timeout: 10_000,
});
waitForNetworkIdle() always waits at least the configured idle time; its documented defaults are zero concurrent connections and 500 milliseconds of idle time. Use it when network quiet is meaningful for your page. If the page polls, streams, or keeps connections open, prefer a selector or another task-specific condition. [Puppeteer page.waitForNetworkIdle()]
5. Configure timeouts deliberately
Navigation and wait operations commonly default to 30,000 milliseconds. Set a per-call timeout when one operation needs a different budget, or set a page-level default for repeated operations.
// Per-call navigation and selector budgets
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 20_000,
});
await page.waitForSelector('[data-ready="true"]', { timeout: 8_000 });
// Page-level wait and navigation defaults
page.setDefaultTimeout(8_000);
page.setDefaultNavigationTimeout(20_000);
Passing 0 disables a timeout and can leave a script waiting indefinitely. Use it only when an unbounded wait is intentional. [Puppeteer wait options]
6. Capture only after the chosen condition
For a screenshot, combine a fast navigation milestone with the exact readiness condition for the content. If the target page marks its finished state with a selector, wait for that marker before capturing.
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('[data-testid="dashboard-ready"]', {
visible: true,
timeout: 15_000,
});
await page.screenshot({ path: 'dashboard.png', fullPage: true });
This keeps the readiness condition tied to what the screenshot needs. Use networkidle0 or networkidle2 instead only when network quiet is a suitable proxy for that page.
7. cURL, Python, and Node.js options
Puppeteer is a Node.js library, so the JavaScript examples above are the direct way to use its page-wait APIs. Python and cURL do not run Puppeteer’s JavaScript API. For those clients, ScreenshotNeo provides a screenshot endpoint that handles browser setup for you.
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(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
For Node.js on a runtime without Bun.write, save the response bytes with your runtime’s filesystem API. See the ScreenshotNeo API documentation for request options and response details.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its one-call endpoint returns a screenshot or PDF, so you do not need to launch and manage a Puppeteer browser for a capture.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Cookie and consent banners are accepted and removed before capture, along with known 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 say which page verdict and billing result applied. An MCP server exposes take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
9. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
Navigation timeout |
The chosen lifecycle event did not occur within the timeout, or the page is slow. | Choose a milestone suited to the task, increase the finite timeout if justified, and inspect whether the page is still loading resources. |
Waiting for selector failed |
The selector is wrong, the content never appeared, or it appeared after the timeout. | Verify the selector in the loaded DOM, confirm the page reached the expected route/state, and adjust the timeout only if the content legitimately takes longer. |
networkidle never completes |
The site keeps polling, streaming, or otherwise maintains network activity. | Wait for the specific content selector or use a less strict threshold such as networkidle2 if it accurately fits the task. |
| The selector resolves but the screenshot is incomplete | The selector represents a shell or placeholder rather than completed content. | Wait for a completed-state marker, a visible result, or a task-specific condition instead. |
A click fails after waitForSelector() |
The wait confirmed presence, but not necessarily all action preconditions; a later action may race with a rerender. | Use a locator for the interaction so Puppeteer waits for the action state, or re-check the element state before acting. |
| Navigation returned a response but the page is an error | HTTP status errors can still produce a navigation response rather than a thrown navigation exception. | Check response?.status() and handle non-success status codes explicitly. |
10. Performance, reliability, and cost
- Use the earliest sufficient condition. Waiting for
loador network idle can add time when your task only needs parsed HTML or one element. - Prefer semantic readiness. A selector for the result you need is less dependent on unrelated network requests than a global quiet-network condition.
- Keep timeouts bounded. A finite per-call budget makes failures visible and prevents a stuck page from consuming a worker indefinitely.
- Check the response. A completed navigation is not the same as a successful HTTP status; handle redirects and error statuses according to the task.
- Account for browser ownership. With Puppeteer, your script manages browser launch and cleanup. A screenshot API removes that browser setup from the client and has explicit plan limits: ScreenshotNeo offers 1,000 monthly free shots, then paid plans from $5 for 3,000.
11. Frequently asked questions
How do I wait for a page to load in Puppeteer?
page.goto(url) waits for load by default. Pass { waitUntil: 'domcontentloaded' }, 'networkidle0', or 'networkidle2' when another lifecycle milestone fits the task.
How do I wait for an element to appear?
Call page.waitForSelector(selector). Add visible: true when it must be visible, and set a timeout that matches the operation’s budget.
Is networkidle0 always the safest option?
No. It can be unsuitable for pages with persistent network connections, and network quiet does not prove that the content you need has rendered. Prefer a task-specific selector when one is available.
What does “ready” mean for a screenshot?
It means the page state you intend to capture is present. For a dynamic page, wait for a marker or element that signals that state before calling page.screenshot().
Can I use Puppeteer from Python or cURL?
Puppeteer’s page APIs are for Node.js. Python and cURL can call a screenshot service such as ScreenshotNeo’s API instead of controlling Puppeteer directly.


