Hero Background

Power Your Software Testing with AI Agents and Cloud

The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.

Selenium PythonAutomationTutorial

Selenium Wait for Page to Load in Python (2026 Guide)

A practical guide to making Selenium wait for a page to load in Python: page load strategies, implicit, explicit, and fluent waits, waits after clicks and AJAX calls, and timeouts, with every example run on a cloud grid.

Last Updated on:

A Selenium Python test that throws NoSuchElementException on one run and passes on the next usually has a timing bug: the script asked for an element before the page, or the JavaScript that renders it, had finished. To make Selenium wait for page to load reliably, wait for a condition instead of a fixed number of seconds. Selenium gives you control at three levels: the page load strategy decides when get() returns, implicit and explicit waits poll for elements, and timeouts cap how long any command can block.

Every example in this guide ran on the TestMu AI cloud Selenium grid on September 29, 2026, and the real console output appears next to the code.

Overview

To wait for a page to load in Selenium Python, use explicit waits with WebDriverWait for dynamic elements or configure global implicit waits for simple pages. These strategies pause script execution until elements are fully loaded, preventing errors like NoSuchElementException and ElementNotInteractableException.

Types of Selenium Waits in Python

  • Implicit Wait: Implicit wait sets one session-wide timeout that every find_element call polls against before it raises NoSuchElementException. It checks presence in the DOM only, not visibility, and its default is 0 seconds.
  • Explicit Wait: WebDriverWait blocks a single call until an expected condition, such as visibility_of_element_located, returns a truthy value, checking every 0.5 seconds by default and raising TimeoutException when time runs out.
  • Fluent Wait: A fluent wait in Python is a WebDriverWait with a custom poll_frequency and extra ignored_exceptions, such as StaleElementReferenceException, for elements that re-render during the wait. Python has no separate FluentWait class.
  • SmartWait: SmartWait is a TestMu AI cloud grid capability, enabled with smartWait in LT:Options, that holds each command until the element passes actionability checks. It applies to the whole session and works only on TestMu AI grid sessions.

What Is Selenium Wait for Page to Load?

Selenium wait for page to load pauses test script execution until a page or DOM element is ready, preventing exceptions from elements that haven't loaded yet.

Selenium sends each command as soon as the previous one returns, but the browser keeps working after the first HTML arrives: scripts render components, fetch data, and attach event handlers. A command that lands in that gap fails with one of these exceptions:

  • NoSuchElementException - the element is not in the DOM yet.
  • ElementNotInteractableException - the element exists but is hidden, disabled, or has no size yet.
  • StaleElementReferenceException - the element was found, then the page re-rendered and replaced it.

A wait holds the next command until a condition you choose is true. Each exception above maps to a specific wait in the table under Common Selenium Wait Errors, and this list of common Selenium exceptions covers the failures that are not about timing.

How Does Selenium Decide a Page Is Loaded?

Selenium uses the page load strategy to map document.readyState: normal waits for complete, eager waits for interactive, and none returns control immediately.

Navigation commands such as driver.get(), back(), forward(), and refresh() block until the new document reaches the document.readyState value that the session's page load strategy asks for. The Selenium waiting strategies documentation states it directly: "All navigation commands wait for a specific readyState value based on the page load strategy (the default value to wait for is 'complete') before the driver returns control to the code."

The readyState values themselves come from the WHATWG HTML standard:

  • loading - the document is still being parsed.
  • interactive - parsing has finished and the DOM is ready, but images, stylesheets, and frames may still be loading.
  • complete - the document and its sub-resources have finished loading.
StrategyreadyState Waited ForWhat It Means
normal (default)completeWaits for the HTML and every sub-resource, such as scripts, stylesheets, images, and frames.
eagerinteractiveReturns once the HTML is parsed and the DOM is ready, while images and stylesheets may still be loading.
none(no wait)Returns right after the navigation starts, without waiting for the document at all.

Set the strategy on the browser options before the session starts:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.page_load_strategy = "eager"  # "normal" (default), "eager" or "none"

Timing get() for the Ecommerce Playground home page on the TestMu AI grid, with Chrome 153 on Windows 11 and a first load in a fresh session each time, gave these results:

Strategyget() Returned AfterreadyState at That Moment
normal3.00 scomplete
eager3.16 scomplete
none0.06 sloading

The eager strategy saved nothing here because this page fired its load event within 50 ms of DOMContentLoaded; eager helps only when images, fonts, or frames keep loading long after the HTML is parsed. The none strategy returned while the document was still loading, so the next command could run against a half-built page.

The readyState Check After get()

A page's own scripts can still be running when get() returns. The Selenium Playground pages used in this guide load their JavaScript through Cloudflare Rocket Loader: the page source references rocket-loader.min.js and gives every script a Rocket Loader type.

The Rocket Loader documentation says it defers "the loading of all of your JavaScript until after rendering."

In one run, get() returned while document.readyState still read "interactive", and it read "complete" 0.31 s later, after the deferred scripts had run; Object.getOwnPropertyDescriptor(document, "readyState") showed a getter defined on the document itself. In the first run for this guide, a click sent right after get() returned never triggered the page's handler. Polling document.readyState after get() closes that gap:

WebDriverWait(driver, 10).until(
    lambda d: d.execute_script("return document.readyState") == "complete"
)

For browser-specific strategy details, see this guide to Selenium page load strategy.

What Are the Different Types of Selenium Waits in Python?

Selenium Python offers three wait types: Implicit Wait (global DOM polling), Explicit Wait (condition-specific), and Fluent Wait (custom polling intervals and exception handling).

Selenium Python has two wait mechanisms, implicitly_wait() on the driver and the WebDriverWait class; a fluent wait is a WebDriverWait with extra settings. For the same waits in Java, see these implicit, explicit, and fluent wait commands.

Wait TypeHow It WorksBest For
Implicit WaitEvery find_element call in the session retries until the element exists or the timeout passes.Small suites on simple pages with predictable load times.
Explicit WaitOne call waits until a specific condition, such as visibility or clickability, is true.Dynamic elements, AJAX content, and anything that appears after an action.
Fluent WaitAn explicit wait with a custom polling interval and extra exceptions to ignore.Elements that re-render during the wait and slow widgets you want to poll less often.

Pick one style per session. The same Selenium documentation warns: "Do not mix implicit and explicit waits. Doing so can cause unpredictable wait times." The mixing demo later in this guide measures how unpredictable.

Implicit Wait in Selenium Python

An implicit wait makes every find_element call retry for up to the given number of seconds before raising NoSuchElementException. It applies to the whole session, and the Selenium documentation lists the default as 0, which means a single attempt with no retry.

driver.implicitly_wait(10)

An implicit wait only checks that an element exists in the DOM. In one run for this guide, it returned the user photo on the Dynamic Data Loading demo after 2.30 s while is_displayed() still returned False, because the image had not finished loading.

Explicit Wait in Selenium Python

An explicit wait blocks one call until an expected condition returns a truthy value, then hands you that value, here the visible element:

photo = WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, "#loading img"))
)

If the condition never succeeds, until() raises TimeoutException. The guide to ExpectedConditions in Selenium covers the built-in conditions with Python examples.

Fluent Wait in Selenium Python

Python has no FluentWait class. A fluent wait is a WebDriverWait with two extra arguments: poll_frequency, how often the condition is re-checked, and ignored_exceptions, the exceptions to swallow between checks.

The WebDriverWait source sets the defaults to 0.5 seconds and NoSuchElementException, and adds any exceptions you pass to that default.

WebDriverWait(
    driver,
    timeout=10,
    poll_frequency=1,
    ignored_exceptions=[StaleElementReferenceException],
).until(EC.invisibility_of_element_located(alert))

This configuration comes from Demo 3 below, where alert is the locator of a message that closes itself. A longer poll interval sends fewer commands but can notice the change up to one interval late, and StaleElementReferenceException belongs in the list when the element re-renders during the wait.

WebDriverWait Parameters and Defaults

These values match Selenium 4.49.0, the version used for every run in this guide:

Parameter or MethodDefaultBehavior
timeoutRequiredSeconds to keep checking before raising TimeoutException.
poll_frequency0.5Seconds to sleep between checks.
ignored_exceptionsNoSuchElementExceptionExceptions swallowed between checks; your list is added to the default.
until(method, message)-Returns the first truthy value the method returns, or raises TimeoutException with your message.
until_not(method, message)-Returns once the method returns a falsy value or raises an ignored exception, or raises TimeoutException.

For the Java version of this class, see Selenium WebDriverWait in Java. The walkthrough below covers the same wait types with Java code:

Youtube thumbnail
Austin Siewert

Austin Siewert

Co-Founder, Steadfast Systems

Discovered @TestMu AI yesterday. Best browser testing tool I've found for my use case. Great pricing model for the limited testing I do 👏

2M+ Devs and QAs rely on TestMu AI

Deliver immersive digital experiences with Next-Generation Mobile Apps and Cross Browser Testing Cloud

How to Implement Selenium Waits for Page to Load?

Use implicitly_wait() for global session waits, WebDriverWait with ExpectedConditions for element-specific waits, or Fluent Wait with custom polling intervals and ignored exceptions.

The demos below run each wait type against pages on the TestMu AI Selenium Playground, through the TestMu AI cloud Selenium grid. The runs used Python 3.12, Selenium 4.49.0, and pytest 9.1.1, with Chrome 153 on Windows 11.

Set Up the Cloud Driver

Put the driver in a pytest fixture in conftest.py. The fixture follows the Python pattern in the TestMu AI Selenium Skill: credentials come from the LT_USERNAME and LT_ACCESS_KEY environment variables, each session is named after its test, and after the test a pytest hook hands the result to the fixture, which reports passed or failed to the TestMu AI dashboard with lambda-status before quitting. If fixtures are new to you, start with this guide to pytest fixtures.

import os

import pytest
from selenium import webdriver


@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    # Keep each phase's report on the test item so the driver fixture can read the result
    report = yield
    setattr(item, f"rep_{report.when}", report)
    return report


@pytest.fixture
def driver(request):
    options = webdriver.ChromeOptions()
    options.browser_version = "latest"
    options.set_capability("LT:Options", {
        "username": os.environ["LT_USERNAME"],
        "accessKey": os.environ["LT_ACCESS_KEY"],
        "platform": "Windows 11",
        "build": "Selenium Python waits",
        "name": request.node.name,
        "video": True,
        "network": True,
    })
    driver = webdriver.Remote(
        command_executor="https://hub.lambdatest.com/wd/hub",
        options=options,
    )
    yield driver
    passed = hasattr(request.node, "rep_call") and request.node.rep_call.passed
    driver.execute_script("lambda-status=" + ("passed" if passed else "failed"))
    driver.quit()

The credentials sit inside LT:Options rather than in the hub URL: with the user name and key embedded in the URL, Selenium 4.49 printed "UserWarning: Embedding username and password in URL could be insecure" for every test. The Python with Selenium guide in the TestMu AI docs lists the other capabilities you can add to LT:Options.

Demo 1: Implicit Wait

The file tests/test_waits.py starts with the imports and an open_page() helper that pairs get() with the readyState check. The first test sets a 10-second implicit wait, clicks Get Random User on the Dynamic Data Loading demo, and looks for the photo the page fetches from an API:

import time

from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

PLAYGROUND = "https://www.testmuai.com/selenium-playground/"


def open_page(driver, path=""):
    driver.get(PLAYGROUND + path)
    # get() returns at the browser's load event; wait for the page's own scripts too
    WebDriverWait(driver, 10).until(
        lambda d: d.execute_script("return document.readyState") == "complete"
    )


def test_implicit_wait(driver):
    driver.implicitly_wait(10)
    open_page(driver, "dynamic-data-loading-demo/")
    driver.find_element(By.ID, "save").click()
    start = time.monotonic()
    photo = driver.find_element(By.CSS_SELECTOR, "#loading img")  # polls up to 10 s
    print(f"\nImplicit wait: photo element found after {time.monotonic() - start:.2f}s, "
          f"displayed={photo.is_displayed()}")

Demo 2: Explicit Wait

The second test repeats the click without an implicit wait and asks WebDriverWait for the photo to be visible, not just present:

def test_explicit_wait(driver):
    open_page(driver, "dynamic-data-loading-demo/")
    driver.find_element(By.ID, "save").click()
    start = time.monotonic()
    photo = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "#loading img"))
    )
    print(f"\nExplicit wait: photo visible after {time.monotonic() - start:.2f}s, "
          f"displayed={photo.is_displayed()}")

Demo 3: Fluent Wait

The third test uses the Bootstrap Alerts demo, whose success message closes itself after 5 seconds. It waits for the alert to appear first, because a wait for invisibility would otherwise pass before the alert was ever shown, then polls once per second until the alert is gone:

def test_fluent_wait(driver):
    alert = (By.CSS_SELECTOR, ".alert-success-auto")
    open_page(driver, "bootstrap-alert-messages-demo/")
    driver.find_element(By.CSS_SELECTOR, "button.btn-success-auto").click()
    WebDriverWait(driver, 5).until(EC.visibility_of_element_located(alert))
    start = time.monotonic()
    WebDriverWait(
        driver,
        timeout=10,
        poll_frequency=1,
        ignored_exceptions=[StaleElementReferenceException],
    ).until(EC.invisibility_of_element_located(alert))
    print(f"\nFluent wait: auto-close alert gone after {time.monotonic() - start:.2f}s")

The Cost of Mixing Implicit and Explicit Waits

The last test puts the warning from the Selenium documentation to a measurement. It waits 5 seconds for an element that does not exist, first with no implicit wait and then with a 10-second one, and finally waits for that missing element to be invisible:

def test_mixing_implicit_and_explicit(driver):
    open_page(driver, "simple-form-demo/")
    missing = (By.ID, "no-such-element")
    print()
    for implicit in (0, 10):
        driver.implicitly_wait(implicit)
        start = time.monotonic()
        try:
            WebDriverWait(driver, 5).until(EC.presence_of_element_located(missing))
        except TimeoutException:
            print(f"implicit={implicit}s + explicit=5s -> TimeoutException after "
                  f"{time.monotonic() - start:.1f}s")
    start = time.monotonic()
    WebDriverWait(driver, 15).until(EC.invisibility_of_element_located(missing))
    print(f"implicit=10s: invisibility of an element that is not there took "
          f"{time.monotonic() - start:.1f}s")

Running pytest -s -v tests/test_waits.py against the grid produced this output:

tests/test_waits.py::test_implicit_wait
Implicit wait: photo element found after 2.78s, displayed=True
PASSED
tests/test_waits.py::test_explicit_wait
Explicit wait: photo visible after 2.92s, displayed=True
PASSED
tests/test_waits.py::test_fluent_wait
Fluent wait: auto-close alert gone after 4.31s
PASSED
tests/test_waits.py::test_mixing_implicit_and_explicit
implicit=0s + explicit=5s -> TimeoutException after 5.3s
implicit=10s + explicit=5s -> TimeoutException after 10.1s
implicit=10s: invisibility of an element that is not there took 10.1s
PASSED

======================== 4 passed in 94.59s (0:01:34) =========================
  • Implicit and explicit - both returned the photo, at 2.78 s and 2.92 s. Only the explicit wait guarantees what it returns is visible; the implicit wait's displayed=True here depended on the image loading in time.
  • Fluent wait - the alert was reported gone 4.31 s after it appeared. With a 1-second poll interval, the wait can notice the change up to a second after it happens.
  • Mixed waits - the 5-second explicit wait timed out at 5.3 s on its own but at 10.1 s once a 10-second implicit wait was set, because each find_element inside the condition blocked for the full implicit timeout before the explicit timer was checked again.
  • Invisibility with an implicit wait - confirming that an absent element is invisible took 10.1 s instead of returning at once, because the lookup had to wait out the implicit timeout before it could report the element missing.
Note

Note: The same suite runs on more than Chrome. TestMu AI Automation Cloud covers 3,000+ browser and OS combinations, so you can swap ChromeOptions for FirefoxOptions or EdgeOptions in the fixture and check that your waits hold up in each browser. Try TestMu AI free

Enhance Selenium Waits with SmartWait on TestMu AI

SmartWait is a TestMu AI grid capability that runs actionability checks on an element before each command and holds the command until those checks pass, up to a limit you set. If they never pass, the command fails with the usual Selenium error. The TestMu AI documentation positions it as a way to "reduce the amount of code dedicated to explicit/implicit waits."

Enable it by adding one key to the LT:Options dictionary in conftest.py:

options.set_capability("LT:Options", {
    "username": os.environ["LT_USERNAME"],
    "accessKey": os.environ["LT_ACCESS_KEY"],
    "platform": "Windows 11",
    "build": "Selenium Python waits",
    "name": request.node.name,
    "video": True,
    "network": True,
    "smartWait": 10,  # seconds; SmartWait accepts 5 to 120
})

To see what it changes, this test clicks Get Random User and calls find_element for the photo with no implicit or explicit wait:

def test_smartwait(driver):
    open_page(driver, "dynamic-data-loading-demo/")
    driver.find_element(By.ID, "save").click()
    start = time.monotonic()
    photo = driver.find_element(By.CSS_SELECTOR, "#loading img")  # no implicit or explicit wait
    print(f"\nphoto element found after {time.monotonic() - start:.2f}s, "
          f"displayed={photo.is_displayed()}")

With SmartWait enabled, the lookup succeeded 2.56 s after the click:

tests/test_smartwait.py::test_smartwait
photo element found after 2.56s, displayed=True
PASSED
============================= 1 passed in 18.72s ==============================

The same test on a session without SmartWait failed at once:

tests/test_smartwait.py::test_smartwait FAILED
E       selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"#loading img"}
============================= 1 failed in 18.77s ==============================
  • Range - smartWait accepts 5 to 120 seconds, and the optional smartWaitRetryDelay sets the retry interval from 1 to 4 seconds.
  • Auto Healing - the Auto Healing documentation states that autoHeal only works when smartWait is disabled, so enable one or the other in a session.
  • Scope - SmartWait runs on the TestMu AI grid, so keep explicit waits in any suite that must also pass on a local browser.

For how the actionability checks work, see Transforming Web Automation With TestMu AI SmartWait.

Why Do Waits Behave Differently in CI?

In CI and on a remote grid, every WebDriver command, including each poll inside a wait, crosses the network, so waits resolve later than they do on a local machine and timeouts tuned locally start to fail.

A condition that becomes true at 1.2 s is only seen as true at the next poll, plus the round trip for that poll's command. Timeouts that fire on a timer stay the same; every step that depends on command round trips gets slower. The comparison below shows both effects.

How Does HyperExecute Improve Wait Reliability?

HyperExecute runs the test code and the browser on the same machine, which removes the network hop from every WebDriver command, so wait timings reflect the application rather than the connection.

HyperExecute is the TestMu AI test orchestration platform. The same two test files ran from a laptop against the cloud grid and then as a HyperExecute job on the same day; the HyperExecute Windows VM ran Chrome 153 on Windows 10:

MeasurementLaptop to Cloud GridHyperExecute
Next page ready after a click0.81 s0.31 s
Old page stale after a click1.64 s0.26 s
Explicit wait: photo visible2.92 s1.49 s
5 s explicit wait with a 10 s implicit wait10.1 s10.0 s
pytest time for tests/test_waits.py94.59 s75.85 s
pytest time for tests/test_navigation_ajax.py107.73 s64.90 s

The steps bound by round trips got faster, while the timer-bound mixed wait took 10 seconds in both places: HyperExecute removes network latency, and a timeout you set still lasts as long as you set it. These are single runs, so read them as an example of the pattern rather than a benchmark.

  • Up to 70% faster - TestMu AI puts HyperExecute at up to 70% faster than traditional grids, because each task runs in one isolated environment with no hub-and-node hops.
  • Auto-split - discovered test files are spread across parallel VMs; the job below ran both files on two machines at once.
  • Retries - retryOnFailure re-runs failed tests up to maxRetries times. Keep the count low so retries do not hide a wait that is too short.
  • Logs per test - terminal output, Selenium, network, and console logs, screenshots, and video are collected for each test, so a timed-out wait can be traced to what the page was doing.

Run the Suite on HyperExecute

Add this hyperexecute.yaml next to conftest.py and a requirements.txt that lists selenium and pytest, then start the job with the HyperExecute CLI, as described in run your first job on HyperExecute:

version: 0.1
globalTimeout: 90
testSuiteTimeout: 90
testSuiteStep: 90

runson: win
autosplit: true
concurrency: 2
retryOnFailure: true
maxRetries: 1

runtime:
  language: python
  version: "3"

pre:
  - pip install -r requirements.txt

testDiscovery:
  type: raw
  mode: remote
  command: grep -rl def.test_ tests

testRunnerCommand: pytest -s -v $test
  • testDiscovery - mode: remote finds the tests on the HyperExecute VM, and grep -rl prints each test file once. The pattern has no quotes or spaces because, in trial runs on the Windows VM, quoted arguments were split apart: a python -c discovery command received only its first word, opening quote included, as the code to run.
  • autosplit and concurrency - with two discovered files and concurrency: 2, the files ran in parallel on separate VMs.
  • runtime - installs Python 3 on each VM before the pre step installs requirements.txt.

The job discovered both files, ran them on two VMs, and completed in 2 minutes 37 seconds:

✔ [1]  discovery (1s)
✔ [3]  tests/test_navigation_ajax.py (1m6s)
✔ [2]  tests/test_waits.py (1m17s)
 COMPLETED
    Test Execution Time:                  1m0s
    Job Duration Time:                    2m37s
    Total Tasks:                          3
    Pass test stage percentage:           100.00%

How to Wait for Page Load After a Click?

After a click, wait for an element that exists only on the next page, or wait for the old page's html element to go stale and then for document.readyState to reach complete.

A click that starts a navigation is only partly covered by Selenium's own waiting. The W3C WebDriver specification tells the browser to allow "any navigations triggered by the click to start" and then wait for them, but it concedes that "some implementations may have unavoidable race conditions."

On the plain link used below, the old page was already stale at the first check after click() returned, in every run for this guide, so the click's own wait covered the navigation. The race bites when navigation starts late, for example from a JavaScript redirect on a timer, and both approaches below still hold then, because they wait for a sign of the new page instead of trusting the click.

Approach 1: Wait for an Element on the Next Page

Wait for something that exists only on the destination, here the message box on the Simple Form Demo page. This is the most reliable option because it waits for exactly what the next step needs:

def test_wait_for_element_on_new_page(driver):
    open_page(driver)
    start = time.monotonic()
    driver.find_element(By.LINK_TEXT, "Simple Form Demo").click()
    WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "user-message")))
    print(f"\nNew page ready after {time.monotonic() - start:.2f}s at {driver.current_url}")
tests/test_navigation_ajax.py::test_wait_for_element_on_new_page
New page ready after 0.81s at https://www.testmuai.com/selenium-playground/simple-form-demo/
PASSED

Approach 2: Wait for the Old Page to Go Stale

When you cannot name an element on the next page, keep a reference to the current html element, click, and wait for staleness_of(). A stale reference proves the browser replaced the document, and the readyState check then waits for the new one to load. This replaces the old habit of sleeping half a second before polling readyState, which reads the old page if the navigation has not started yet.

def test_staleness_after_click(driver):
    open_page(driver)
    old_page = driver.find_element(By.TAG_NAME, "html")
    start = time.monotonic()
    driver.find_element(By.LINK_TEXT, "Simple Form Demo").click()
    clicked = time.monotonic() - start
    WebDriverWait(driver, 10).until(EC.staleness_of(old_page))
    stale = time.monotonic() - start
    WebDriverWait(driver, 10).until(
        lambda d: d.execute_script("return document.readyState") == "complete"
    )
    print(f"\nclick() returned after {clicked:.2f}s, old page stale after {stale:.2f}s, "
          f"new page complete after {time.monotonic() - start:.2f}s")
tests/test_navigation_ajax.py::test_staleness_after_click
click() returned after 1.41s, old page stale after 1.64s, new page complete after 1.77s
PASSED

Single-page apps that change routes without loading a new document never make the html element stale. Use Approach 1 there, or EC.url_changes() when the route change itself is what you need to confirm.

How to Wait for AJAX Requests to Complete?

Wait for the loading indicator to appear and then disappear, wait for the element your test needs, or count in-flight fetch and XHR calls yourself; Resource Timing cannot see requests that are still running.

document.readyState can reach complete long before the data a page fetches afterwards arrives, so on data-driven pages the page load wait is only the start. The Dynamic Data Loading demo follows the usual pattern: clicking Get Random User shows "loading...", fetches a user from an API, then renders the name and photo.

Strategy 1: Wait for the Loading Indicator to Disappear

Wait for the indicator to appear before waiting for it to go. A wait that only checks for disappearance can pass before the indicator is ever shown:

def test_wait_for_loading_indicator(driver):
    status = (By.ID, "loading")
    open_page(driver, "dynamic-data-loading-demo/")
    driver.find_element(By.ID, "save").click()
    start = time.monotonic()
    WebDriverWait(driver, 5).until(EC.text_to_be_present_in_element(status, "loading"))
    WebDriverWait(driver, 15).until_not(EC.text_to_be_present_in_element(status, "loading"))
    print(f"\n'loading...' indicator gone after {time.monotonic() - start:.2f}s")
tests/test_navigation_ajax.py::test_wait_for_loading_indicator
'loading...' indicator gone after 1.75s
PASSED

Strategy 2: Wait for the Target Element

Waiting for the element the test needs skips any guess about what the page is doing in the meantime; the test continues as soon as the photo is visible:

def test_wait_for_target_element(driver):
    open_page(driver, "dynamic-data-loading-demo/")
    driver.find_element(By.ID, "save").click()
    start = time.monotonic()
    WebDriverWait(driver, 15).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "#loading img"))
    )
    print(f"\nUser photo visible after {time.monotonic() - start:.2f}s")
tests/test_navigation_ajax.py::test_wait_for_target_element
User photo visible after 2.77s
PASSED

Strategy 3: Count In-Flight Requests

When a page shows no indicator, count its requests. The script below wraps fetch and XMLHttpRequest to keep a counter on window; install it after the page loads and before the action that starts the requests. The Resource Timing check is included for comparison:

TRACK_REQUESTS = """
if (window.__pendingRequests === undefined) {
  window.__pendingRequests = 0;
  const originalFetch = window.fetch;
  window.fetch = (...args) => {
    window.__pendingRequests++;
    return originalFetch(...args).finally(() => window.__pendingRequests--);
  };
  const originalSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.send = function (...args) {
    window.__pendingRequests++;
    this.addEventListener("loadend", () => window.__pendingRequests--);
    return originalSend.apply(this, args);
  };
}
"""

RESOURCE_TIMING_CHECK = """
return window.performance.getEntriesByType('resource')
    .filter(r => !r.responseEnd).length === 0;
"""


def test_pending_requests(driver):
    open_page(driver, "dynamic-data-loading-demo/")
    driver.execute_script(TRACK_REQUESTS)  # install before the action that fires requests
    driver.find_element(By.ID, "save").click()
    start = time.monotonic()
    print(f"\nResource Timing check right after the click: "
          f"{driver.execute_script(RESOURCE_TIMING_CHECK)}, "
          f"requests in flight: {driver.execute_script('return window.__pendingRequests')}")
    WebDriverWait(driver, 15).until(
        lambda d: d.execute_script("return window.__pendingRequests === 0")
    )
    print(f"No requests in flight after {time.monotonic() - start:.2f}s")
    WebDriverWait(driver, 5).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "#loading img"))
    )
    print(f"User photo visible after {time.monotonic() - start:.2f}s")
tests/test_navigation_ajax.py::test_pending_requests
Resource Timing check right after the click: True, requests in flight: 1
No requests in flight after 0.73s
User photo visible after 2.44s
PASSED

The Resource Timing check, which an earlier version of this article recommended, returned True, meaning idle, while a request was still in flight.

That is by design. Entries are created by the mark resource timing step of the Resource Timing specification, which the WHATWG Fetch standard runs only after a response body has been received, so a running request has no entry in performance.getEntriesByType('resource') yet.

The counter also reached zero before the photo was visible, at 0.73 s against 2.44 s. The photo is an image the app inserts after the data arrives, and the browser downloads it outside fetch and XHR, so finish with an element wait.

How to Set Page Load and Script Timeouts in Selenium?

Waits decide when to continue; timeouts cap how long a single command may block. The W3C WebDriver specification defines three session timeouts, and a new session on the grid reported its defaults: 0 seconds for implicit waits, 300 seconds for page loads, and 30 seconds for scripts.

def test_timeouts(driver):
    print(f"\nDefault timeouts: {driver.timeouts.implicit_wait}s implicit, "
          f"{driver.timeouts.page_load}s page load, {driver.timeouts.script}s script")
    driver.set_page_load_timeout(30)
    driver.set_script_timeout(15)
    open_page(driver)
    start = time.monotonic()
    driver.execute_async_script("""
        const done = arguments[arguments.length - 1];
        setTimeout(done, 5000);
    """)
    print(f"Async script finished after {time.monotonic() - start:.2f}s")
    driver.set_script_timeout(2)
    start = time.monotonic()
    try:
        driver.execute_async_script("setTimeout(arguments[arguments.length - 1], 5000);")
    except TimeoutException:
        print(f"TimeoutException after {time.monotonic() - start:.2f}s with a 2 s script timeout")
tests/test_navigation_ajax.py::test_timeouts
Default timeouts: 0.0s implicit, 300.0s page load, 30.0s script
Async script finished after 5.06s
TimeoutException after 2.06s with a 2 s script timeout
PASSED
  • set_page_load_timeout(seconds) - caps navigation commands such as get(); if the readyState your page load strategy waits for is not reached in time, the command raises TimeoutException.
  • set_script_timeout(seconds) - caps execute_async_script(); the run above raised TimeoutException after 2.06 s with a 2-second limit.
  • Where to set them - in the fixture or right after creating the driver, so every test in the session runs under the same limits.

Common Selenium Wait Errors and How to Fix Them

In Selenium automation suites, most wait failures surface as one of these exceptions, and each has a specific fix. For exception handling beyond waits, see how to handle errors and exceptions in Selenium Python.

ExceptionRoot CauseFix
NoSuchElementExceptionThe element was not in the DOM when Selenium looked for it.Wait with presence_of_element_located, or set an implicit wait, but not both in one session.
ElementNotInteractableExceptionThe element exists but is hidden, disabled, or has no size yet.Wait with visibility_of_element_located or element_to_be_clickable instead of presence_of_element_located.
StaleElementReferenceExceptionThe DOM re-rendered after the element reference was captured.Locate the element inside the wait condition, or add StaleElementReferenceException to ignored_exceptions.
TimeoutExceptionThe condition was not met within the timeout.Check the locator, check whether an implicit wait is inflating the timeout, then raise the timeout if the page is genuinely slow.
ElementClickInterceptedExceptionAnother element, such as a modal or cookie banner, covers the target.Wait for the overlay with invisibility_of_element_located before clicking.

Which Selenium Wait Should You Use?

Use explicit WebDriverWait for most waits, a readyState check after get() on script-heavy pages, staleness_of() after clicks that load a new page, and target-element waits rather than network checks on AJAX pages.

ScenarioRecommended Approach
Small suite, simple pages, consistent load timesimplicitly_wait() set once, with no explicit waits in the same session
Element that appears after an actionWebDriverWait + EC.visibility_of_element_located
Element you are about to clickWebDriverWait + EC.element_to_be_clickable
Page scripts still running after get()WebDriverWait + a document.readyState == 'complete' check
Click that loads a new pageAn element on the next page, or EC.staleness_of(old_html) then the readyState check
Route change in a single-page appAn element on the new view, or EC.url_changes()
Loading spinner or status textWait for it to appear, then for it to disappear
Element that re-renders during the waitWebDriverWait with ignored_exceptions=[StaleElementReferenceException]
Requests with no visible signalAn in-flight fetch and XHR counter, followed by an element wait
Heavy pages on slow networksset_page_load_timeout() with a buffer above the slowest normal load

Start from explicit waits scoped to what the next step needs. Keep implicit waits for small suites with stable pages, and never in the same session as explicit waits: the mixing demo showed a 5-second wait taking 10 seconds. The Selenium tutorial hub collects guides on the rest of the Selenium API.

How Can AI Coding Assistants Write Reliable Selenium Waits?

If Claude Code, GitHub Copilot, Cursor, or Gemini CLI writes your Selenium tests, the Selenium Skill gives the assistant the same wait rules this guide measured. It is part of the open-source TestMu AI agent skills collection, and one command adds it to a project:

npx agentskillsforall add https://github.com/LambdaTest/agent-skills.git --skill selenium-skill

Its rules line up with the demos above:

  • Explicit waits only - the skill marks its wait strategy as critical and tells the assistant to "ALWAYS use explicit waits".
  • No sleeps, no mixing - its anti-pattern table flags sleeps as "Flaky, slow" and implicit plus explicit waits as "Unpredictable timeouts", the effect the mixing demo measured at 10.1 seconds for a 5-second wait.
  • Cloud-ready fixtures - its Python reference includes a pytest fixture for the TestMu AI grid that reads LT_USERNAME and LT_ACCESS_KEY from the environment, and its cloud reference reports pass or fail to the dashboard with lambda-status, as the fixture in this guide does.

Install the Selenium Skill so Claude Code, Copilot, and Cursor write explicit waits instead of sleeps.

Selenium

Conclusion

Open the flakiest test in your suite, replace each time.sleep() and each unguarded find_element with an explicit wait for the element the next line uses, and add the readyState check to your page-opening helper. Then run the suite on the TestMu AI Selenium testing tool with the setup from the pytest with Selenium documentation.

Author

...

Navin Chandra

Blogs: 5

  • Linkedin

Navin Chandra is a Member of Technical Staff at TestMu AI (formerly LambdaTest), building the open-source automation that powers its Selenium and Appium cloud grid. A committer to both Selenium and Appium, he implemented WebDriver BiDi support in Selenium for real-time browser events and bidirectional control and is developing Apple's iOS RemoteXPC protocol in Appium to enable low-level wireless communication with iOS system services. He contributes to Selenium across multiple language bindings as a member of the Selenium GitHub organization. He has served as a Google Summer of Code mentee and mentor at openSUSE and an LFX mentee at CNCF's KubeArmor, and is a SUSE Certified Deployment Specialist. Navin holds a B.Tech in Computer Science.

Reviewer

...

Srinivasan Sekar

Reviewer

  • Linkedin

Srinivasan Sekar is Director of Engineering at TestMu AI (formerly LambdaTest), where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.

Selenium Wait for Page Load FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests