Skip to content

Playwright wait for page load: load states, selectors and network idle

On a page that loads its products after the load event, Playwright's default goto() and networkidle both returned with 0 of 10 products. Eleven waits, timed.

HProxy TeamOctober 12, 2026Updated October 12, 20267 min read

Playwright wait for page load: load states, selectors and network idle

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(), a wait_for_function condition, 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.
  • networkidle returned 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() and all_text_contents() do not wait. Read a list only after the content is there.
  • For scraping, take the response the page asks for. expect_response had 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.

Terminal output of wait_tests.py: eleven ways to wait in Playwright, each timed on three test pages, with the number of products on the page when the wait returned and the two networkidle timeouts
Our own capture, 12 October 2026: wait_tests.py on our server, Playwright 1.63.0 and Chromium 153

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 products0.04 s, 0 products0.06 s, 0 products
domcontentloaded0.09 s, 00.02 s, 00.04 s, 0
commit0.06 s, 00.05 s, 00.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 API1.09 s2.30 s2.35 s
wait_for_function (10 products)1.19 s2.42 s2.40 s
locator.wait_for()1.46 s2.50 s2.50 s
page.wait_for_selector()1.43 s2.55 s2.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.

Frequently asked questions

How do I wait for a page to fully load in Playwright?

No load state means fully loaded. page.goto() waits for the load event by default, and a script can add content after it. Wait for the element you need with page.locator(...).wait_for(), or for the response that brings the data. In our test, goto() returned with 0 of 10 products on a page that loaded them later.

What is the difference between load, domcontentloaded, commit and networkidle?

domcontentloaded finishes with the DOMContentLoaded event, load with the load event, and commit as soon as the response arrives and the document starts loading. networkidle waits for 500 ms without any network connection. None of them waits for the content itself.

Why does networkidle time out in Playwright?

Because the page keeps a request open or keeps calling the server. networkidle needs 500 ms without any network connection. On our test page with one request always open, it timed out after 10 seconds every time. Playwright's documentation marks networkidle as discouraged.

What can I use instead of networkidle?

Wait for the content itself: page.locator(...).wait_for(), page.wait_for_function() with a condition of your own, or page.expect_response() for the request that brings the data. In our test, all three returned with all 10 products on all three pages, including the one where networkidle timed out.

How do I stop goto() from waiting for the page load?

Pass wait_until="commit" or wait_until="domcontentloaded" to page.goto(). In our test, both returned in under 0.1 seconds on all three pages. Then wait for the element you need before you read it.

Why does locator.count() return 0 in Playwright?

Because count() does not wait. It counts the elements on the page at that moment. In our test it returned 0 right after goto() and 10 after locator.wait_for(). all_text_contents() and all() also return at once.

Is wait_for_timeout bad for scraping?

A fixed wait is too long on a fast page and too short on a slow one. In our test, 2 seconds came on top of the load on one page and ended too early, with 0 of 10 products, on another. Playwright's documentation says never to wait for timeout in production.

Should I use wait_for_selector or locator.wait_for?

locator.wait_for(). Playwright's documentation marks page.wait_for_selector as discouraged and points to locator-based waits. In our test both returned with all 10 products, at similar times.

Get proxies that are alive right now

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed