page.goto() in Playwright waits for the page's load event by default. On many pages that is the wrong moment. A script may fetch the content after the load event, and a slow image can hold the event back after the content is there.
We built three test pages with known timings and timed eleven ways to wait on each. The test ran on our own server on 12 October 2026, with Playwright 1.63.0 for Python and its Chromium 153.
The short answer
- Wait for the content itself. Use
page.locator(".product").first.wait_for(), await_for_functioncondition, or the response that carries the data. - The default
goto()returned with 0 of 10 products on a page that loaded them after its load event. networkidlereturned too early on that page and never on another. Playwright's documentation marks it as discouraged.- A fixed
wait_for_timeout(2000)was too long on one page and too short on another. count()andall_text_contents()do not wait. Read a list only after the content is there.- For scraping, take the response the page asks for.
expect_responsehad the data as early as any other wait.
The three test pages
Each page asks our test server for ten products with JavaScript, and the server answers after 0.7 seconds.
- /early asks for the products after 0.3 seconds, so they arrive after about 1.1 seconds. A slow image holds the load event until about 1.5 seconds.
- /late has no slow image, so its load event comes at once. Its script waits 1.5 seconds before it asks for the products, so they cannot arrive before 2.2 seconds.
- /live is /late plus long polling. It keeps one request to the server open at all times, as live feeds do.
For each way to wait, we measured the seconds from page.goto() until the wait returned. We also counted the products on the page at that moment. Each number is the middle of three runs.

Load states: what goto() waits for
goto() takes a wait_until option, and its documentation lists four values. "'load' - consider operation to be finished when the load event is fired". That is the default. domcontentloaded stops at the DOMContentLoaded event, and commit stops "when network response is received and the document started loading".
None of them knows about content that a script adds later.
| wait_until | /early | /late | /live |
|---|---|---|---|
| load (default) | 1.58 s, 10 products | 0.04 s, 0 products | 0.06 s, 0 products |
| domcontentloaded | 0.09 s, 0 | 0.02 s, 0 | 0.04 s, 0 |
| commit | 0.06 s, 0 | 0.05 s, 0 | 0.09 s, 0 |
On /early, the default wait got all ten products, but only because the slow image held the load event. On /late, it returned at once, with nothing. The fix is to wait for the element you want to read.
from playwright.sync_api import sync_playwright
URL = "http://127.0.0.1:8000/late" # the page to load
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(URL)
print("after goto():", page.locator(".product").count(), "products")
page.locator(".product").first.wait_for()
print("after wait_for():", page.locator(".product").count(), "products")
browser.close()
after goto(): 0 products
after wait_for(): 10 products
locator.wait_for() "Returns when element specified by locator satisfies the state option". Its default state waits until the element is visible.
networkidle: too early or never
networkidle sounds like the safe choice. The documentation describes it with a warning. "'networkidle' - DISCOURAGED consider operation to be finished when there are no network connections for at least 500 ms".
Some guides say it allows two open connections. That rule comes from Puppeteer. Its docs say networkidle2 "Waits till there are no more than 2 network connections for at least 500 ms". Playwright's wait_until has no such value.
Our test showed both ways it goes wrong. On /late, it returned after 0.62 seconds with 0 products, because the script waited 1.5 seconds before it asked for them. The network was quiet in that gap. On /live, one request was always open, so the network never went quiet.
from playwright.sync_api import TimeoutError, sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
try:
page.goto("http://127.0.0.1:8000/live", wait_until="networkidle", timeout=10_000)
except TimeoutError as e:
print(str(e).splitlines()[0])
browser.close()
Page.goto: Timeout 10000ms exceeded.
page.wait_for_load_state("networkidle") after a plain goto() behaved the same way: too early on /late, a timeout on /live. Without our 10-second limit, Playwright waits for its default of 30 seconds before it gives up.
In the capture above, both timeouts show 10 products. They had arrived long before, but the wait still ended in an error.
Wait for the element or the data instead
Four waits returned with all ten products on all three pages.
| Way to wait | /early | /late | /live |
|---|---|---|---|
expect_response for the API | 1.09 s | 2.30 s | 2.35 s |
wait_for_function (10 products) | 1.19 s | 2.42 s | 2.40 s |
locator.wait_for() | 1.46 s | 2.50 s | 2.50 s |
page.wait_for_selector() | 1.43 s | 2.55 s | 2.65 s |
page.wait_for_selector() still works, but the documentation now marks it "Discouraged". It says "Use web assertions that assert visibility or a locator-based locator.wait_for() instead". With state="attached", which skips the visibility check, it was no faster in our runs.
wait_for_function checks a condition of your own. Its polling "Defaults to raf", which means it checks again on every frame the browser draws.
page.goto(URL, wait_until="domcontentloaded")
page.wait_for_function("document.querySelectorAll('.product').length >= 10")
print(page.locator(".product").count(), "products")
10 products
For scraping, you may not need the rendered page at all. The script on our test pages asks /api/products for JSON, and expect_response hands you that response as it arrives.
with page.expect_response("**/api/products") as response_info:
page.goto(URL, wait_until="domcontentloaded")
products = response_info.value.json()
print(len(products), "products from the API, the first:", products[0])
10 products from the API, the first: {'name': 'Lamp 1'}
The goto() sits inside the with block, so the wait starts before the request goes out. Our guide to parsing JSON in Python covers what to do with the data next.
Fixed waits
page.wait_for_timeout(2000) after the default goto() took 3.60 seconds on /early. The default wait alone already had all ten products at 1.58 seconds. On /late, it ended after 2.06 seconds, before the products arrived, with 0 of 10. The documentation is blunt about it. "Never wait for timeout in production. Tests that wait for time are inherently flaky".
What waits on its own
Actions wait by themselves. Before a click or a fill, "Playwright performs a range of actionability checks on the elements before making actions". Reading one element waits too. text_content() on a locator waits until that element exists.
Reading a whole list does not wait. count() and all_text_contents() return whatever is on the page at that moment.
page.goto(URL)
print(page.locator(".product").all_text_contents())
print(page.locator(".product").first.text_content())
[]
Lamp 1
The documentation says all() "immediately returns whatever is present in the page". So use one of the waits above before you read a list.
Through a proxy
Through a proxy, every request makes an extra hop, so a page can take longer to fill. Waits that watch the element or the response just return later, as long as their timeout leaves room. A fixed wait keeps its length and ends too early more often. Our guide to proxies with Playwright shows how to pass a proxy to launch().
What this page could not check
- Three test pages of our own on one server. Real pages run more scripts and send more requests.
- Chromium 153 through Playwright 1.63.0 for Python only. Firefox and WebKit were not tested.
- Each time is the middle of three runs. Between our two full runs, the times on /early and /late moved by up to 0.17 seconds.
- We will check this page again by 12 January 2027.
Sources
- Playwright for Python, Page (goto, wait_for_load_state, wait_for_selector, wait_for_function, wait_for_timeout): playwright.dev.
- Playwright for Python, Locator (wait_for, all, count, all_text_contents, text_content): playwright.dev.
- Playwright for Python, auto-waiting: playwright.dev.
- playwright 1.63.0 on PyPI: pypi.org.
- Puppeteer, PuppeteerLifeCycleEvent (networkidle0 and networkidle2): github.com.
- Our own tests on our server, 12 October 2026: wait_tests.py at 09:37 and 09:47 UTC, and the code on this page at 10:03 UTC, on test pages of our own.



