Explicit Wait in Selenium: WebDriverWait Complete Guide (2026)

Explicit Wait in Selenium: WebDriverWait Complete Guide (2026)

Explicit wait in Selenium uses WebDriverWait(driver, timeout).until(condition) to pause execution until a specific condition is met. Use expected_conditions (EC) for common conditions: EC.presence_of_element_located, EC.visibility_of_element_located, EC.element_to_be_clickable. Never mix explicit and implicit waits — use explicit waits exclusively.

Key Takeaways

Explicit wait is the only correct wait strategy. Implicit waits apply globally and interact unpredictably with explicit waits. time.sleep() wastes time. Use explicit waits with specific conditions for every element interaction.

Choose the right expected condition. presence_of_element_located (element in DOM), visibility_of_element_located (element visible), element_to_be_clickable (visible + enabled). Wrong condition = flaky tests.

Poll interval defaults to 500ms. WebDriverWait checks the condition every 500ms. For faster checks, set poll_frequency=0.1. For slow elements, increase the timeout, not the poll frequency.

TimeoutException tells you what went wrong. Always add descriptive messages: WebDriverWait(..., message="Checkout button not clickable after 10s"). This saves debugging time.

Custom conditions handle complex waiting logic. When built-in conditions aren't enough, write a lambda or class-based condition. For example, wait for element text to match a regex.

What is Explicit Wait in Selenium?

Explicit wait tells Selenium: "Wait up to N seconds for this specific condition to be true, polling every 500ms."

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

wait = WebDriverWait(driver, 10)  # Max 10 seconds
element = wait.until(EC.element_to_be_clickable((By.ID, "submit-btn")))
element.click()

Compare this to the bad alternatives:

# ❌ time.sleep — always waits the full duration, even if element appears in 0.5s
import time
time.sleep(5)
element = driver.find_element(By.ID, "submit-btn")

# ❌ implicit wait — global, interacts badly with explicit waits
driver.implicitly_wait(10)  # Set once, applies to ALL find_element calls

WebDriverWait Parameters

from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(
    driver=driver,
    timeout=10,              # Max seconds to wait
    poll_frequency=0.5,     # Check every 0.5 seconds (default)
    ignored_exceptions=[     # Don't fail on these exceptions
        NoSuchElementException,
        StaleElementReferenceException,
    ]
)

Common configurations:

# Fast-polling wait (for responsive apps)
fast_wait = WebDriverWait(driver, 5, poll_frequency=0.1)

# Long wait for slow operations
slow_wait = WebDriverWait(driver, 30)

# Wait that ignores stale elements
stable_wait = WebDriverWait(
    driver, 10,
    ignored_exceptions=[StaleElementReferenceException]
)

Expected Conditions Reference

Presence and Visibility

from selenium.webdriver.support import expected_conditions as EC

# Element exists in DOM (may be hidden)
EC.presence_of_element_located((By.ID, "element"))

# Element exists and is visible (display != none, visibility != hidden)
EC.visibility_of_element_located((By.ID, "element"))

# Element is visible (pass the WebElement directly)
EC.visibility_of(already_found_element)

# Element is NOT visible (hidden or removed)
EC.invisibility_of_element_located((By.ID, "loading-spinner"))

# All elements matching selector are present
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".list-item"))

Clickability and Interactivity

# Element is visible and enabled (ready to click)
EC.element_to_be_clickable((By.ID, "submit-btn"))

# Element is selected (checkbox/radio)
EC.element_to_be_selected(element)

# Element is NOT selected
EC.element_located_to_be_selected((By.ID, "checkbox"))

URL and Title

# URL equals exactly
EC.url_to_be("https://example.com/dashboard")

# URL contains substring
EC.url_contains("/dashboard")

# URL matches regex
EC.url_matches(r".*\/dashboard\/\d+")

# Title equals exactly
EC.title_is("Dashboard - MyApp")

# Title contains substring
EC.title_contains("Dashboard")

Text Content

# Element's text equals
EC.text_to_be_present_in_element((By.ID, "status"), "Complete")

# Element's text contains substring
EC.text_to_be_present_in_element((By.ID, "counter"), "items")

# Input value contains text
EC.text_to_be_present_in_element_value((By.ID, "search"), "query")

Frames and Windows

# Wait for iframe and switch into it
EC.frame_to_be_available_and_switch_to_it((By.ID, "payment-iframe"))
EC.frame_to_be_available_and_switch_to_it(0)  # By index

# New browser tab/window opened
EC.new_window_is_opened(current_handles)

# Number of windows equals N
EC.number_of_windows_to_be(2)

Alert

# JavaScript alert is present
EC.alert_is_present()

Element State

# Element exists and is stale (DOM was refreshed)
EC.staleness_of(element)

# Element has specific attribute value
EC.element_attribute_to_include((By.ID, "btn"), "class")

Practical Examples

Wait for Page Load After Navigation

driver.get("https://example.com/products")

# Wait for a key element that confirms the page loaded
product_list = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".product-grid"))
)

Wait for AJAX Content

# Click a button that loads data asynchronously
driver.find_element(By.ID, "load-more").click()

# Wait for new items to appear
items = wait.until(
    EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".product-card"))
)
assert len(items) > 0

Wait for Loading Spinner to Disappear

# Trigger an action
driver.find_element(By.ID, "save-button").click()

# Wait for spinner to appear (optional, confirms request started)
wait.until(EC.visibility_of_element_located((By.ID, "loading-spinner")))

# Wait for spinner to disappear
wait.until(EC.invisibility_of_element_located((By.ID, "loading-spinner")))

# Now interact with the result
success_message = wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".success-toast"))
)
assert "saved" in success_message.text.lower()

Wait for Modal to Open

# Click button that opens modal
driver.find_element(By.ID, "delete-btn").click()

# Wait for modal
modal = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal")))

# Click confirm inside modal
confirm_btn = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, ".modal .btn-danger"))
)
confirm_btn.click()

# Wait for modal to close
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal")))

Wait for URL Change After Redirect

# Submit form
driver.find_element(By.ID, "submit").click()

# Wait for redirect
wait.until(EC.url_contains("/success"))
# or
wait.until(EC.url_to_be("https://example.com/order/12345/success"))

Wait for Text to Update

# Trigger update
driver.find_element(By.ID, "refresh-status").click()

# Wait for status text to change
wait.until(
    EC.text_to_be_present_in_element((By.ID, "order-status"), "Shipped")
)

Custom Wait Conditions

When built-in conditions aren't enough, write custom ones:

Lambda Condition

# Wait for element count to reach a specific number
wait.until(lambda d: len(d.find_elements(By.CSS_SELECTOR, ".item")) >= 10)

# Wait for JavaScript variable to be set
wait.until(lambda d: d.execute_script("return window.appReady === true"))

# Wait for element to have a specific CSS class
wait.until(lambda d: "active" in d.find_element(By.ID, "tab").get_attribute("class"))

# Wait for element attribute value
wait.until(
    lambda d: d.find_element(By.ID, "progress").get_attribute("value") == "100"
)

Class-Based Condition

For reusable, parameterizable conditions:

class element_has_class:
    """Wait for element to have a specific CSS class."""
    def __init__(self, locator, css_class):
        self.locator = locator
        self.css_class = css_class

    def __call__(self, driver):
        try:
            element = driver.find_element(*self.locator)
            return self.css_class in element.get_attribute("class")
        except:
            return False


class element_count_exceeds:
    """Wait for element count to exceed a threshold."""
    def __init__(self, locator, count):
        self.locator = locator
        self.count = count

    def __call__(self, driver):
        elements = driver.find_elements(*self.locator)
        return len(elements) > self.count


# Usage
wait.until(element_has_class((By.ID, "submit-btn"), "btn-primary"))
wait.until(element_count_exceeds((By.CSS_SELECTOR, ".result-item"), 5))

Error Handling

TimeoutException

from selenium.common.exceptions import TimeoutException

try:
    element = wait.until(EC.visibility_of_element_located((By.ID, "result")))
    element.click()
except TimeoutException:
    print(f"Element not found after {10}s")
    print(f"Current URL: {driver.current_url}")
    # Take screenshot for debugging
    driver.save_screenshot("timeout_debug.png")
    raise

Adding Descriptive Messages

# WebDriverWait accepts a message parameter for better error output
from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(driver, 10)

element = wait.until(
    EC.element_to_be_clickable((By.ID, "checkout-button")),
    message="Checkout button was not clickable after 10 seconds on checkout page"
)

When this times out, the error message will include your description — much easier to debug than a generic TimeoutException.

Explicit Wait vs Implicit Wait vs FluentWait

Feature Explicit Wait Implicit Wait Fluent Wait
Scope Per-element Global Per-element
Condition Specific EC Element exists Custom
Poll frequency Configurable Browser default Configurable
Exception handling Configurable NoSuchElementException Configurable
Reliability High Low High
Recommendation ✅ Use this ❌ Avoid ✅ For complex cases

Never mix implicit and explicit waits. When you set driver.implicitly_wait(10) and also use WebDriverWait, behavior becomes unpredictable. Some waits compound, some don't.

# ❌ Never do this
driver.implicitly_wait(10)
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.ID, "element")))

# ✅ Do this — explicit waits only
driver.implicitly_wait(0)  # Disable implicit waits
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.ID, "element")))

Common Mistakes

Mistake 1: Using presence when you need visibility

# ❌ Element might be hidden
element = wait.until(EC.presence_of_element_located((By.ID, "modal-content")))
element.click()  # ElementNotInteractableException — element is hidden!

# ✅ Wait for visibility
element = wait.until(EC.visibility_of_element_located((By.ID, "modal-content")))
element.click()

Mistake 2: Short timeout on slow actions

# ❌ 2 seconds too short for page load
wait = WebDriverWait(driver, 2)
element = wait.until(EC.url_contains("/dashboard"))

# ✅ Use appropriate timeout
wait = WebDriverWait(driver, 15)
element = wait.until(EC.url_contains("/dashboard"))

Mistake 3: Waiting after already finding the element

# ❌ Find element, THEN wait — race condition if DOM updates
element = driver.find_element(By.ID, "status")
wait.until(EC.text_to_be_present_in_element(element, "Done"))

# ✅ Pass locator tuple, not element
wait.until(EC.text_to_be_present_in_element((By.ID, "status"), "Done"))

Mistake 4: Reusing WebDriverWait after timeout

# ❌ After TimeoutException, the wait object is exhausted
try:
    wait.until(EC.visibility_of_element_located((By.ID, "slow-element")))
except TimeoutException:
    pass
# Reusing wait here may not start a fresh 10-second window

# ✅ Create a new WebDriverWait instance for each critical wait

Full Test Example with Explicit Waits

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


def test_purchase_flow(driver):
    wait = WebDriverWait(driver, 10)

    # 1. Load product page
    driver.get("https://shop.example.com/product/123")
    wait.until(EC.title_contains("Product"))

    # 2. Add to cart
    add_to_cart = wait.until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='add-to-cart']"))
    )
    add_to_cart.click()

    # 3. Wait for cart count to update
    wait.until(
        EC.text_to_be_present_in_element((By.ID, "cart-count"), "1")
    )

    # 4. Go to checkout
    checkout_btn = wait.until(
        EC.element_to_be_clickable((By.ID, "checkout-button"))
    )
    checkout_btn.click()
    wait.until(EC.url_contains("/checkout"))

    # 5. Fill shipping info
    name = wait.until(EC.visibility_of_element_located((By.ID, "full-name")))
    name.send_keys("Test User")

    # 6. Submit order
    place_order = wait.until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-testid='place-order']"))
    )
    place_order.click()

    # 7. Wait for confirmation
    wait = WebDriverWait(driver, 30)  # Longer wait for payment processing
    wait.until(EC.url_contains("/confirmation"))

    order_number = wait.until(
        EC.visibility_of_element_located((By.ID, "order-number"))
    )
    assert order_number.text.startswith("ORD-")

The Alternative: AI Handles Waits Automatically

Every Selenium test requires explicit wait logic. Miss one wait and you get flaky tests. HelpMeTest removes this entirely:

Go to https://shop.example.com/product/123
Click the "Add to Cart" button
Verify the cart shows 1 item
Click "Checkout"
Fill "Test User" in the name field
Click "Place Order"
Verify an order confirmation number is shown

The AI waits for elements automatically — it won't click until the element is actually interactive, just like a human. No WebDriverWait, no expected_conditions, no TimeoutException debugging.

Explicit waits remain essential when:

  • Maintaining large existing Selenium codebases
  • Testing complex timing scenarios specifically
  • Need precise control over wait conditions
  • Framework or tooling requires Selenium

When to reconsider:

  • Spending more time debugging flaky waits than writing tests
  • Non-technical team members need to write tests
  • Tests break after every UI update

Summary

Explicit wait best practices:

  1. Always use WebDriverWait + expected_conditions — never time.sleep()
  2. Disable implicit waits: driver.implicitly_wait(0)
  3. Choose the right EC: presence < visibility < element_to_be_clickable
  4. Add descriptive message parameters for easier debugging
  5. Use custom lambdas for conditions not covered by EC
  6. Match timeout to actual operation speed (10s for UI, 30s for slow APIs)

Read more