Resource library

QA How-To

How to Fix Selenium TimeoutException in WebDriverWait

Fix Selenium TimeoutException in WebDriverWait by checking locators, conditions, frames, shadow DOM, timing, and CI differences with runnable Python examples.

22 min read | 2,773 words

TL;DR

A WebDriverWait TimeoutException means its condition never became truthy before the explicit deadline. Confirm the action happened, inspect the live locator and browsing context, choose the right expected condition, then adjust timing only if the correct state arrives too late.

Key Takeaways

  • Inspect the exact until() condition, locator, current URL, and preceding action before changing timeouts.
  • Use presence, visibility, clickability, text, or invisibility according to the state the workflow needs.
  • Relocate elements after a rerender, and switch into an iframe or open shadow root before searching there.
  • Keep implicit wait at zero when using targeted explicit waits so durations remain understandable.
  • Measure a healthy transition in CI before increasing its wait budget.
  • Capture URL, title, match count, and screenshot where the failing wait expires.

If you need to fix Selenium TimeoutException in WebDriverWait, the error appears when an explicit wait never observes its requested condition before the deadline. Check the locator, current page, browsing context, and action that should produce the state before increasing the timeout.

selenium.common.exceptions.TimeoutException: Message:

That is the minimal Python exception line when until() has no custom message; driver stack details may follow. Passing a message as the second argument to until() adds it after Message:. The examples below use Python and a local data: page, so you can reproduce each cause without depending on a changing public site. Selenium's WebDriverWait reference defines timeout in seconds and a default polling interval of 0.5 second.

TL;DR

Use a locator-based expected condition that matches the next observable state, after performing the action that creates that state:

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

button = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-testid='save']"))
)
button.click()

element_to_be_clickable checks visibility and enabled state; an overlay may still intercept the subsequent click. If the wait expires, print driver.current_url, count matches for the locator, capture a screenshot, and inspect the active frame. Increase the budget only after proving the correct state arrives late. The Selenium waiting strategies guide warns that implicit and explicit waits can interact unexpectedly.

What the Error Actually Means

WebDriverWait(driver, timeout).until(predicate) calls the predicate repeatedly with the driver. A truthy result ends the wait and is returned. A false result is retried after the poll interval until the deadline. Python's wait ignores NoSuchElementException by default so an element may appear later; it does not suppress every exception. When the requested state never becomes true, until() raises TimeoutException. The exception tells you which wait failed, not why the application failed to reach the state.

Read the stack trace to confirm the exception came from until(). driver.get() can raise a timeout for page loading, and execute_async_script() uses a separate script timeout. Changing driver.set_page_load_timeout() does not lengthen WebDriverWait; changing a WebDriverWait does not repair navigation. The WebDriver timeout API documents those independent settings.

Presence asks if a node exists in the DOM. Visibility additionally requires displayed content with nonzero dimensions. Clickability asks for visibility and enabled state. Text changes, invisibility, URL changes, frame availability, and staleness answer different questions. The wrong condition can expire even with a correct locator. Use the expected conditions reference to select the predicate your workflow actually needs.

Root-Cause Decision Table

Symptom Root cause Fix
find_elements returns zero despite a visible control Wrong locator or page Check URL and inspect live DOM for a stable selector
Element exists but wait for clickability expires Hidden or disabled element Wait for the appropriate state and its trigger
Result appears only after input Triggering action never ran Perform the action before waiting for its result
A cached element stops reflecting the page DOM node was replaced Relocate with a locator-based condition
Control is visible inside an iframe Wrong browsing context Wait for and switch into that frame
Component content cannot be found Open shadow-root boundary Find the host, enter its root, then locate
Correct state appears after the deadline Genuine slow transition Measure and set a justified budget
Wall time exceeds expected wait Mixed implicit and explicit waits Set implicit wait to zero
Local passes, CI or Grid fails Browser or environment differs Capture URL, screenshot, match count, and timings in CI

Treat each row as a hypothesis, then run its verification command. For a complete surrounding project, see the Selenium Python framework tutorial.

1. Fix Selenium TimeoutException in WebDriverWait Caused by a Wrong Locator

Set up a shared helper so every case below can run in a fresh browser session. Install Python's Selenium package in a virtual environment and make Chrome available on the machine. Selenium Manager normally resolves a compatible driver for a local browser, so you do not need to paste a hard-coded driver path. Save this helper as wait_lab.py:

python -m venv .venv
. .venv/bin/activate
python -m pip install selenium
python -c "import selenium; print(selenium.__version__)"
# wait_lab.py
import os
from urllib.parse import quote
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def open_page(markup):
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1280,800")
    remote_url = os.environ.get("SELENIUM_REMOTE_URL")
    if remote_url:
        driver = webdriver.Remote(command_executor=remote_url, options=options)
    else:
        driver = webdriver.Chrome(options=options)
    driver.get("data:text/html;charset=utf-8," + quote(markup))
    return driver

First verify the helper, not the wait:

python -c "from wait_lab import open_page; d=open_page('<title>Wait lab</title>'); print(d.title); d.quit()"

Expect Wait lab. A browser-creation error must be fixed separately. Now save case_locator.py to verify that a selector matches the DOM actually loaded:

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

driver = open_page('<button data-testid="save">Save</button>')
try:
    locator = (By.CSS_SELECTOR, "button[data-testid='save']")
    print("matches:", len(driver.find_elements(*locator)))
    button = WebDriverWait(driver, 3).until(EC.presence_of_element_located(locator))
    assert button.text == "Save"
    print("locator: PASS")
finally:
    driver.quit()
python case_locator.py

Expect matches: 1 and locator: PASS. Temporarily replace save with submit to see the count become zero and the wait expire. On a real route, inspect rendered DOM after navigation rather than copying a class from source code. Generated classes and translated text often change. Prefer stable test attributes for controls your team owns. If the current URL is a login page, fix authentication or navigation; a broader selector might match an unrelated element and produce a misleading pass.

2. Fix Selenium TimeoutException in WebDriverWait Caused by the Wrong Condition

A node can exist while CSS hides it or an attribute disables it. presence_of_element_located can pass before an action is possible, while visibility_of_element_located will never pass for an intentionally hidden input. This fixture starts with a disabled button and enables it shortly afterward. Save case_condition.py:

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

html = '<button id="save" disabled>Save</button>'
driver = open_page(html)
try:
    locator = (By.ID, "save")
    present = WebDriverWait(driver, 3).until(EC.presence_of_element_located(locator))
    assert present.is_displayed() and not present.is_enabled()
    driver.execute_script("setTimeout(() => document.querySelector('#save').disabled = false, 300)")
    ready = WebDriverWait(driver, 3).until(EC.element_to_be_clickable(locator))
    assert ready.is_enabled()
    ready.click()
    print("clickable state: PASS")
finally:
    driver.quit()
python case_condition.py

Expect clickable state: PASS. For a spinner that must disappear, use EC.invisibility_of_element_located((By.ID, "spinner")); for status text, use EC.text_to_be_present_in_element(locator, "Ready"). An invisibility wait may pass because the node was removed, so follow it with an assertion on the desired destination state. If a later click raises ElementClickInterceptedException, investigate the covering layer using the intercepted click guide. That error has a different cause from an expired wait.

3. Start the Action That Produces the Awaited State

A wait observes application behavior; it does not cause the behavior. A result panel triggered by a button will remain unchanged if a previous conditional branch skipped the click or used the wrong control. Keep the trigger adjacent to the result wait so the sequence is clear. Save case_trigger.py:

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

html = '''<button id="load" onclick="setTimeout(() => {
  document.querySelector('#result').textContent = 'Loaded';
}, 250)">Load</button><p id="result">Waiting</p>'''
driver = open_page(html)
try:
    wait = WebDriverWait(driver, 3)
    wait.until(EC.element_to_be_clickable((By.ID, "load"))).click()
    wait.until(EC.text_to_be_present_in_element((By.ID, "result"), "Loaded"))
    assert driver.find_element(By.ID, "result").text == "Loaded"
    print("trigger and result: PASS")
finally:
    driver.quit()
python case_trigger.py

Expect trigger and result: PASS. Remove .click() to reproduce an expired result wait. On an application that fetches data, inspect the request and response in browser tools or server logs. A successful click followed by a failed API response is an application failure, not a reason for more polling. The Selenium API response waiting guide covers the network-specific case.

4. Relocate Elements Replaced by a Dynamic Render

A JavaScript framework can replace a DOM node while keeping its ID and visible text area. A WebElement cached before the render points to the removed node. Querying that old reference may raise StaleElementReferenceException, and it will not track the replacement. Use a locator-based condition so each poll searches the current DOM. Save case_replaced.py:

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

html = '<p id="status">Pending</p>'
driver = open_page(html)
try:
    old_node = driver.find_element(By.ID, "status")
    assert old_node.text == "Pending"
    driver.execute_script("setTimeout(() => document.querySelector('#status').outerHTML = `<p id='status'>Ready</p>`, 250)")
    WebDriverWait(driver, 3).until(
        EC.text_to_be_present_in_element((By.ID, "status"), "Ready")
    )
    assert driver.find_element(By.ID, "status").text == "Ready"
    print("replacement observed: PASS")
finally:
    driver.quit()
python case_replaced.py

Expect replacement observed: PASS. To prove a node was replaced, EC.staleness_of(old_node) is another valid condition; locate the successor afterward. Do not add every WebDriver exception to ignored_exceptions, because that can conceal a lost browser session or bad selector. Selenium's common errors guide distinguishes stale references, changed context, and navigation. Cached page-object fields are especially vulnerable when the app rerenders controls.

5. Switch Into the Correct Iframe Before Searching

driver.find_element searches the active browsing context. A button drawn inside an iframe is not in the top document, even if a screenshot shows it on the same screen. Wait for the frame and switch into it, then locate the inner control. Save case_frame.py:

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

html = '<iframe id="editor" srcdoc="<button id=\'save\'>Save</button>"></iframe>'
driver = open_page(html)
try:
    wait = WebDriverWait(driver, 3)
    assert len(driver.find_elements(By.ID, "save")) == 0
    wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "editor")))
    button = wait.until(EC.element_to_be_clickable((By.ID, "save")))
    button.click()
    assert button.text == "Save"
    driver.switch_to.default_content()
    print("iframe context: PASS")
finally:
    driver.quit()
python case_frame.py

Expect iframe context: PASS. A nested frame requires switching through each parent in order. After a tab or modal change, verify the active context again. Return to default_content() before searching for a top-level control. If the frame wait itself expires, inspect the frame locator and whether the page inserted the iframe, rather than changing the inner button's timeout.

6. Enter an Open Shadow Root Before Locating Its Content

A top-level CSS query does not cross a shadow boundary. A web component can render a button in an open shadow root while driver.find_elements(By.ID, "save") reports zero. Selenium's shadow_root property and ShadowRoot.find_element API give you access to that internal tree. Save case_shadow.py:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from wait_lab import open_page

html = '''<div id="panel"></div><script>
const root = document.querySelector('#panel').attachShadow({mode: 'open'});
setTimeout(() => root.innerHTML = '<button id="save">Save</button>', 250);
</script>'''
driver = open_page(html)
try:
    assert len(driver.find_elements(By.ID, "save")) == 0
    button = WebDriverWait(driver, 3).until(
        lambda d: d.find_element(By.ID, "panel").shadow_root.find_element(By.ID, "save")
    )
    assert button.text == "Save"
    print("open shadow root: PASS")
finally:
    driver.quit()
python case_shadow.py

Expect open shadow root: PASS. The default wait ignores NoSuchElementException until the button is inserted. If the host appears asynchronously, the same lambda also retries the host lookup. A closed shadow root cannot be traversed with this public Selenium API; test its exposed behavior instead. See the shadow DOM locator examples for nested component cases.

7. Measure a Real Delay Before Extending the Wait

The correct element may eventually appear, but only after a transition longer than your current budget. Measure from the action to the observed state under the same browser and machine. timeout is in seconds; poll_frequency is the delay between checks. A shorter interval can notice a quick change earlier, but it also sends more WebDriver commands and cannot fix a state that never arrives. Save case_budget.py:

from time import monotonic
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from wait_lab import open_page

html = '<p id="state">Starting</p>'
driver = open_page(html)
try:
    started = monotonic()
    driver.execute_script("setTimeout(() => document.querySelector('#state').textContent = 'Ready', 800)")
    WebDriverWait(driver, 3, poll_frequency=0.1).until(
        EC.text_to_be_present_in_element((By.ID, "state"), "Ready")
    )
    elapsed = monotonic() - started
    print(f"ready after {elapsed:.2f}s")
    assert driver.find_element(By.ID, "state").text == "Ready"
finally:
    driver.quit()
python case_budget.py

Expect an elapsed-time line near the illustrative 800 ms timer plus browser overhead; the exact number varies. Repeat the measurement in CI before raising its budget. Allow for normal slower runs, but investigate growing server latency or browser resource contention. A very large global timeout makes genuine regressions slow to identify. See reducing flaky tests in CI for suite-level strategies.

8. Remove Implicit Waits That Distort Explicit-Wait Timing

An implicit wait applies to each element search. An explicit wait may perform many such searches. If each lookup has its own long implicit allowance, wall-clock time can exceed the explicit deadline you thought you set. Prefer zero implicit wait and a focused explicit wait around the particular transition. Save case_implicit.py:

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

driver = open_page('<p id="ready">Ready</p>')
try:
    driver.implicitly_wait(0)
    assert driver.timeouts.implicit_wait == 0
    element = WebDriverWait(driver, 3).until(
        EC.visibility_of_element_located((By.ID, "ready"))
    )
    assert element.text == "Ready"
    print("explicit-only wait: PASS")
finally:
    driver.quit()
python case_implicit.py

Expect explicit-only wait: PASS. Search framework fixtures and page-object constructors for implicitly_wait(...); its setting persists for the driver session and may be far from the failing test. A zero implicit setting makes a bad direct lookup fail promptly with a clearer stack trace. A fixed time.sleep() is no substitute: it always waits the full duration on fast runs and still fails when the transition exceeds that duration.

9. Reproduce the CI or Docker Browser Environment

A test may pass locally while CI's headless browser sees a different viewport, login state, route, or service response. until() is where the mismatch surfaces, but the runner's page is the key evidence. Capture diagnostics at the failure. Save case_diagnostics.py; TARGET_URL can point to your application, while the fallback fixture makes the code runnable unchanged:

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

driver = open_page('<button id="save">Save</button>')
try:
    target = os.environ.get("TARGET_URL")
    if target:
        driver.get(target)
    try:
        WebDriverWait(driver, 5).until(
            EC.visibility_of_element_located((By.ID, "save")),
            "save button did not become visible"
        )
        print("target visible: PASS")
    except TimeoutException:
        print("current URL:", driver.current_url)
        print("title:", driver.title)
        print("save matches:", len(driver.find_elements(By.ID, "save")))
        driver.save_screenshot("wait-timeout.png")
        raise
finally:
    driver.quit()
python case_diagnostics.py

Expect target visible: PASS. With TARGET_URL set, inspect the screenshot and URL on failure; a redirected login or browser error page changes the diagnosis immediately. Preserve screenshots as CI artifacts. The Selenium screenshot on failure guide covers this pattern in a larger suite.

For a local Docker Grid diagnostic, start the official standalone Chrome image using a published tag that matches the Selenium release and browser combination you validated. Replace <matching-selenium-version> with that tag; never assume a copied version still exists:

docker run --rm -d --name selenium-wait-grid -p 4444:4444 --shm-size=2g selenium/standalone-chrome:<matching-selenium-version>
curl -fsS http://localhost:4444/status
SELENIUM_REMOTE_URL=http://localhost:4444 python case_diagnostics.py

When Python runs in another container, localhost refers to that container. Put the test and Grid containers on a shared network and use the Grid service hostname in SELENIUM_REMOTE_URL. The helper's webdriver.Remote branch keeps the test identical across local Chrome and Grid. Selenium's Grid setup guide documents port 4444 and /status. Stop the diagnostic container with docker stop selenium-wait-grid when finished. In CI, run python case_diagnostics.py after the browser service is ready and upload the screenshot on failure. The Docker for Selenium Grid tutorial covers a fuller deployment.

How to Verify the Fix

Run the case matching your suspected root cause, then repeat that command in the environment where the original failure occurred. Each local fixture prints PASS or an elapsed time and exits nonzero if an assertion or wait fails. Once you have saved all snippets, run this sequence to check browser setup, selectors, contexts, and timing together:

python case_locator.py
python case_condition.py
python case_trigger.py
python case_replaced.py
python case_frame.py
python case_shadow.py
python case_budget.py
python case_implicit.py
python case_diagnostics.py

In the real application, replace the fixture with the actual route and assert the business result after until() succeeds. A green wait proves only that its predicate became truthy; it does not prove a save persisted or a workflow completed. Use a fresh browser session to remove cookies or leftover tabs as confounders. On the failing runner, compare elapsed time, current URL, title, locator match count, and screenshot against a passing run. If the state never appears, another arbitrary timeout increase is not verification.

The same diagnosis applies to Java although its syntax differs: WebDriverWait takes a Duration, and ExpectedConditions provides locator-based predicates. The Selenium waits scenario interview guide has Java-specific examples. Keep language-specific code inside its matching test project.

Prevent It From Coming Back

Give application controls stable locators, ideally explicit test attributes when your team owns the markup. Place the triggering action and its result wait in the same test step. Choose a condition that represents the user-visible transition: visibility for rendered content, clickability for a ready control, text for updated data, invisibility for a completed spinner, or staleness for a replaced node. Add a post-wait assertion for the outcome the test actually cares about.

Document one explicit-wait policy in the framework and keep implicit wait at zero if helpers depend on targeted explicit waits. Measure occasional slow transitions under CI load and inspect application or Grid saturation before raising budgets. Keep a screenshot and current URL with failures. Do not swallow TimeoutException and continue, because subsequent actions then fail farther from the original cause.

Align local and CI browser conditions: intended viewport, authentication, test data, feature flags, and service availability. Update a page object when content moves into an iframe or shadow root. Prefer fresh locators over cached WebElement fields when the UI rerenders. A concise custom until() message can identify the expected business state, but it should supplement the locator and diagnostic evidence.

Interview Questions and Answers

Q: What does a WebDriverWait TimeoutException prove?

The supplied predicate did not return a truthy value before that explicit wait's deadline. It does not prove the application was slow. I inspect the predicate, live locator, page context, and preceding action before changing timing.

Q: Why can presence pass while a click fails?

Presence only proves a DOM node exists. The control could be hidden, disabled, or covered. I wait for the state needed by the action, then diagnose an intercepted click separately if it occurs.

Q: How do you handle a node replaced during polling?

I use a locator-based condition to find the current node on each poll. If replacement itself matters, I wait for the old reference to become stale before locating its successor. I avoid caching the pre-render element.

Q: Why does a visible iframe button time out?

A screenshot combines multiple browsing contexts, but WebDriver searches the active one. I wait for the frame, switch into it, and then find the button. I return to default content for later top-level actions.

Q: Does a top-level CSS locator search inside shadow DOM?

No. For an open root, I locate its host, access shadow_root, and query inside it. For a closed root, I test the component's exposed behavior instead of relying on internal selectors.

Q: Why can mixing implicit and explicit waits distort the deadline?

Each poll may perform a find operation with its own implicit delay. The total wall time can exceed the explicit number. I set implicit wait to zero and use targeted waits where the transition occurs.

Common Mistakes

  • Increasing every timeout before checking whether the locator matches on the current page.
  • Waiting for a post-click result before issuing the click.
  • Using presence when the next action needs a visible or enabled control.
  • Reusing a WebElement across a rerender and expecting it to follow the replacement.
  • Searching iframe or shadow content from the top document.
  • Catching TimeoutException without retaining the URL, screenshot, and locator.
  • Treating a headless-only failure as proof that the browser merely needs more time.
  • Confusing explicit waits with page-load and asynchronous-script timeouts.
  • Using a fixed sleep to cover a transition with variable latency.

Conclusion

To fix Selenium TimeoutException in WebDriverWait, find the condition that stayed false and identify why. Confirm the live locator and browsing context, perform the trigger, choose the condition that reflects the desired state, and measure timing only after those checks pass. Rerun the focused case in the original failing environment and keep diagnostic evidence for the next regression.

Interview Questions and Answers

How would you triage a Selenium WebDriverWait TimeoutException?

I locate the exact until() call and identify its predicate. Then I check the preceding action, current URL, active frame, and live locator matches. Only after the target is confirmed do I measure timing and reconsider the budget.

What is the difference between presence and visibility conditions?

Presence checks that a node exists in the DOM. Visibility requires displayed content with nonzero dimensions. I choose according to whether the next step needs a rendered control or merely a node to inspect.

Does element_to_be_clickable guarantee a successful click?

No. It checks visibility and enabled state, but an overlay can still intercept the actual click. I treat a later ElementClickInterceptedException as a separate layout or state problem and inspect what covers the target.

How do you wait for an element after a React rerender?

I use a locator-based condition so each poll searches the current DOM. If I have an old WebElement, I can wait for staleness before finding the new one. I avoid querying a reference tied to a removed node.

Why can an iframe cause a timeout despite a visible target?

WebDriver searches the active browsing context, while a screenshot shows content from several contexts. I wait for the iframe, switch into it, and locate the target. I switch back when the next action belongs to the top page.

How would you wait for content in an open shadow root?

I locate the host, access its shadow_root, and locate the child from that root inside the wait predicate. A top-level CSS selector does not cross the boundary. For a closed root, I test the exposed behavior.

Why is a large implicit wait troublesome with an explicit wait?

Each explicit poll may perform a find operation subject to the implicit wait, making elapsed time longer than expected. I set implicit wait to zero and state an explicit timeout around the relevant transition.

What evidence would you capture for a CI-only timeout?

I record the current URL, title, locator match count, screenshot, and elapsed time at the failed wait. Those distinguish wrong navigation, login redirects, DOM differences, and genuine slowness. I reproduce with the same browser service before changing limits.

Frequently Asked Questions

What causes TimeoutException in Selenium WebDriverWait?

The predicate passed to until() stayed false through the explicit deadline. Common causes include a wrong locator, missing trigger, incorrect condition, different frame, shadow root, or slow transition. Inspect the current page and exact wait call to distinguish them.

How do I increase the WebDriverWait timeout in Python?

Pass a larger number of seconds to WebDriverWait(driver, timeout), such as WebDriverWait(driver, 15). First prove the target state appears under the correct locator. A longer wait cannot repair a missing trigger or wrong frame.

What is WebDriverWait's default polling interval?

The Python constructor defaults to a 0.5-second interval. You can set poll_frequency explicitly, such as 0.1, when faster observation is justified. More polls add WebDriver traffic and do not cure a condition that remains false.

Does WebDriverWait ignore NoSuchElementException?

Yes, Python's wait ignores NoSuchElementException by default while polling. Other exceptions are not automatically ignored. Add ignored_exceptions only for an understood transient state rather than masking all failures.

Why does an on-screen element remain unfound by WebDriverWait?

It may be inside an iframe or open shadow root while WebDriver searches the top document. The current page may also be wrong, or the locator may target old markup. Check the URL and live DOM, then enter the correct context.

Should I mix implicit and explicit waits?

Avoid the combination when predictable timing matters. An implicit wait applies inside each find operation, including those used by an explicit condition. Set implicit wait to zero and use targeted explicit waits.

Why does WebDriverWait time out only in CI?

CI can use a different viewport, authentication state, browser service, or slower app response. Save a screenshot and print the current URL, title, match count, and elapsed time at failure. Compare those facts with the passing local run before changing a timeout.

Related Guides