Puppeteer and Playwright waitUntil Options Explained
Understand what each Puppeteer and Playwright waitUntil option waits for, when to use it, and how to avoid flaky readiness checks.
Short answer: Puppeteer and Playwright both default navigation waits to load. Puppeteer supports load, domcontentloaded, networkidle0, and networkidle2. Playwright supports load, domcontentloaded, networkidle, and commit. Choose the event that matches what the next step needs; network quiet alone does not prove that an application is ready.
What waitUntil means
waitUntil tells a navigation operation which browser lifecycle milestone to wait for before it resolves. It is a navigation timing condition, not an assertion that a particular button, result, or application state is usable.
Both libraries default their navigation wait to load. The option names differ between libraries, so use the values documented for the specific framework and method. Puppeteer WaitForOptions and Playwright Page API document the respective defaults and accepted values.
Option comparison
| Intent | Puppeteer | Playwright | What it tells you |
|---|---|---|---|
| Document parsing completed | domcontentloaded |
domcontentloaded |
The document’s DOMContentLoaded event fired. Images, subframes, and other resources may still be loading, and a client-rendered app may not yet show the content you need. |
| Document load event | load |
load |
The browser’s load event fired after the document and dependent resources reached that event boundary. This is the default in both libraries. |
| Network quiet | networkidle0 or networkidle2 |
networkidle |
Puppeteer exposes two concurrent-connection thresholds. Playwright exposes one state: no network connections for at least 500 ms. It is not an application-readiness assertion. |
| Response received and loading started | Not a documented lifecycle value | commit |
Playwright resolves when the response is received and the document begins loading. It is useful when the workflow will perform a more specific wait afterward. |
Puppeteer’s network-idle lifecycle events use a 500 ms idle period: networkidle0 means no more than zero active connections and networkidle2 means no more than two. Playwright’s networkidle also uses a 500 ms quiet period. These are API definitions, not performance guarantees. See Puppeteer lifecycle events and Playwright Page API.
How to choose a wait condition
- Identify the next operation’s requirement. If it only needs a parsed document, use
domcontentloaded. If the browser load event is itself the required boundary, useload. - For Playwright, use
commitfor an early navigation boundary. Then wait for the actual content or state the workflow needs. - Prefer a selector or assertion for application readiness. If the next step needs a visible result or usable button, wait for that result or button. A page can be quiet while an app is still waiting on a timer, and it can keep making background requests after its useful content is ready.
- Use network idle only when network quiet is the actual requirement. Playwright explicitly discourages using
networkidlefor tests and recommends web assertions to assess readiness.
In Playwright, locators and web assertions provide the state-based synchronization commonly needed by tests. Playwright auto-waits for relevant action conditions; a separate waitForLoadState is often unnecessary. Playwright Frame API explains load-state waits and auto-waiting.
Runnable Playwright examples
This example uses Playwright Test and a state-based assertion. Save as wait.spec.js in a project with @playwright/test installed, then run it with npx playwright test wait.spec.js. Replace the example URL and selector with the page under test.
const { test, expect } = require('@playwright/test');
test('wait for the search results the test needs', async ({ page }) => {
await page.goto('https://example.com/search?q=browser', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
// Synchronize on the application state required by this test.
await expect(page.locator('[data-testid="search-results"]')).toBeVisible();
});
For an early navigation boundary, then an explicit target:
await page.goto('https://example.com/dashboard', {
waitUntil: 'commit',
timeout: 30_000,
});
await page.locator('h1').waitFor({ state: 'visible' });
commit is accepted by Playwright navigation methods such as page.goto(). page.waitForLoadState() accepts only load, domcontentloaded, and networkidle; it applies to an already committed navigation and resolves immediately if that state has already occurred. Do not pass commit to waitForLoadState().
Runnable Puppeteer examples
This CommonJS example opens a page, waits for DOM parsing, and checks for the actual result element. Save it as capture.js and run with node capture.js in a project where puppeteer is installed.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com/search?q=browser', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await page.waitForSelector('[data-testid="search-results"]', {
visible: true,
timeout: 10_000,
});
console.log('Search results are visible');
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Puppeteer accepts a single lifecycle event or an array. With an array, navigation waits until every listed event has fired:
await page.goto('https://example.com', {
waitUntil: ['domcontentloaded', 'load'],
timeout: 30_000,
});
This array is useful only when both milestones are required; it waits at least as long as the slowest listed event. The documented default timeout for Puppeteer’s WaitForOptions is 30 seconds. You can set a per-navigation timeout, or configure page-level navigation timeout defaults with Puppeteer’s page timeout settings. See WaitForOptions.
waitForLoadState versus navigation waitUntil
In Playwright, page.goto(url, { waitUntil }) waits during navigation. page.waitForLoadState(state) waits for a lifecycle state associated with a navigation that has already committed. It is not a replacement for a locator wait when the test needs specific content.
Puppeteer also has page.waitForNetworkIdle(), a separate method with its own options. Do not assume that method’s option names or defaults are interchangeable with page.goto({ waitUntil }). Consult the relevant method reference for the installed version.
Common mistakes and fixes
| Problem | Cause | Fix |
|---|---|---|
networkidle0 or networkidle2 rejected in Playwright |
These are Puppeteer lifecycle labels; Playwright documents only networkidle. |
Use the Playwright value, or better, wait for the specific content/state needed. |
commit rejected by Puppeteer |
commit is a Playwright navigation value, not a documented Puppeteer lifecycle event. |
Use a Puppeteer lifecycle value such as domcontentloaded, then wait for a selector if needed. |
Playwright rejects commit in waitForLoadState |
waitForLoadState supports only load, domcontentloaded, and networkidle. |
Use commit with a navigation method such as goto, or use a supported state with waitForLoadState. |
| Navigation times out at network idle | Long-lived requests, polling, streaming, or other activity can prevent the quiet condition. | Choose a document milestone and then wait for the needed selector or app state. |
| Navigation resolves, but the content is missing | A lifecycle event does not promise that client-side rendering or a later data request has completed. | Wait for the target locator, selector, or application-specific readiness condition. |
| Navigation waits longer than expected | load includes the load-event boundary, which may be later than DOM parsing or response commit. |
Use an earlier milestone only if the following steps explicitly wait for what they need. |
| Puppeteer array wait takes longer than one event | An array requires all listed events. | Remove milestones the workflow does not need; retain all only when each is a real requirement. |
Performance, reliability, and cost
There is no universal fastest option: completion time depends on the page and the event selected. In general, commit can let a Playwright workflow proceed before document lifecycle events, while domcontentloaded precedes load. Using an early event is reliable only if later operations wait for their own required state.
Waiting for networkidle adds a quiet-period requirement and may be a poor fit for pages with continuing background traffic. Waiting for a specific locator can make the condition match the task more closely. Avoid adding arbitrary fixed delays as a substitute for a state signal: they can waste time on fast pages and still fail on slow ones.
For screenshot workflows, the same distinction applies: a lifecycle event is only one possible readiness boundary. If you want to avoid maintaining browser setup for routine captures, ScreenshotNeo provides a one-request screenshot API and MCP server. Its billing rules mean bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; each response identifies the page verdict and billing status.
Or skip the browser setup
For a screenshot, call the API with a URL. Full options and configuration are in the ScreenshotNeo docs.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed before capture; 60+ known consent platforms, newsletter popups, and chat widgets can be removed, and each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; response headers report the page verdict and billing status.
- An MCP server lets AI agents, including Claude and Cursor, call
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. All features are on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Are Puppeteer’s networkidle0 and Playwright’s networkidle equivalent?
No. Puppeteer has zero-connection and two-or-fewer-connection thresholds; Playwright documents one no-connections state. Do not substitute one label for another.
Which waitUntil should I use for a Playwright test?
Use the earliest navigation milestone that suits the flow, then assert the actual page state the test requires. Playwright recommends web assertions for readiness instead of relying on network idle.
Does domcontentloaded mean my single-page app is ready?
No. It means the document parsing event fired. Wait separately for the app content or state your next operation depends on.
Can I pass several waitUntil values in Playwright?
The array behavior described here is documented for Puppeteer. Playwright’s navigation option documents a single wait state; do not assume the Puppeteer array form applies.


