July 24, 2026
Selenium Waits Explained with Code Examples
Learn Selenium waits explained with practical code examples for implicit wait, explicit wait, and fluent wait, plus how to avoid timing bugs and flaky tests.
If you have written enough Selenium tests, you already know the pattern: the test passes locally, fails in CI, and the failure disappears when you re-run it. That is usually not a “Selenium problem” so much as a timing problem. The browser is doing work, the page is doing work, your test is doing work, and the assertion is arriving before the UI is ready.
That is what people usually mean when they ask for Selenium waits explained: how do you make the test wait for the right thing, without turning every step into a blind sleep that slows the suite down and still flakes sometimes?
The short version is this:
- Implicit waits change how long Selenium searches for elements by default.
- Explicit waits wait for a specific condition, like visibility, clickability, or text.
- Fluent waits are an explicit wait with more control over polling and ignored exceptions.
If you remember only one thing, remember this: wait for the application state you need, not for an arbitrary number of seconds.
Why timing failures happen in Selenium
Modern UIs are asynchronous by default. A click can trigger:
- an API request,
- a React/Vue/Angular render cycle,
- a spinner or skeleton screen,
- a DOM update,
- a layout shift,
- a toast notification,
- a background navigation.
Selenium is not looking at your intent. It only sees the DOM, the browser state, and the commands you send. So a test can fail because it tries to click an element before it exists, assert text before it is rendered, or read a list before the AJAX call finishes.
A common failure mode is this:
- The element exists in the HTML.
- It is not visible yet.
- It is visible but still disabled.
- It is clickable but covered by an overlay.
- It is updated by JavaScript after your assertion runs.
That is why waits are not a nice-to-have, they are part of the test’s contract with the application.
Good waits make timing explicit. Bad waits hide uncertainty.
The three Selenium wait types at a glance
Before diving into code, it helps to separate the jobs each wait type does.
Implicit wait
An implicit wait tells WebDriver to keep trying to find elements for a set amount of time before throwing NoSuchElementException.
Use it for a small, consistent default if your team really wants one. The tradeoff is that it applies broadly, which makes debugging and timing behavior less precise.
Explicit wait
An explicit wait waits for a condition on a specific element or page state. This is the workhorse for most production test suites.
Use it when a test depends on a real UI condition, such as:
- element visible,
- element clickable,
- text present,
- URL changed,
- page title updated,
- attribute changed.
Fluent wait
A fluent wait is an explicit wait with control over the polling interval and ignored exceptions. It is useful when the default polling frequency is too coarse or when you need to ignore specific transient failures.
Use it when the target state is noisy or when a condition may fail transiently during polling.
Selenium implicit wait, what it does and what it does not do
In Selenium Python, an implicit wait is set on the driver and applies to element lookup calls.
from selenium import webdriver
browser = webdriver.Chrome() browser.implicitly_wait(10)
browser.get(“https://example.com”) button = browser.find_element(“css selector”, “button.save”) button.click()
The driver will keep trying to locate button.save for up to 10 seconds.
When implicit wait helps
Implicit waits can reduce brittle failures when an element appears a moment after navigation or after a simple interaction. If your app has a few minor rendering delays, a small implicit wait can mask them.
The problem with implicit wait
Implicit wait looks simple, but it has sharp edges:
- It affects every element lookup.
- It can increase the duration of failures in unexpected places.
- It does not wait for visibility, clickability, or text, only presence during element lookup.
- It can interact poorly with explicit waits and make timing harder to reason about.
A common production issue is that teams set a global implicit wait, then add explicit waits later. The result is confusing behavior, because every polling attempt in the explicit wait can inherit the implicit delay. That can make waits longer than expected and obscure the actual root cause.
Practical opinion
If you are starting a new suite, I would avoid relying on implicit waits as your main synchronization strategy. A small implicit wait can be acceptable in legacy suites, but explicit waits are usually easier to reason about and debug.
Selenium explicit wait, the most useful pattern in practice
Explicit waits are the default choice for stable UI automation.
In Python, you usually use WebDriverWait with expected conditions.
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
browser = webdriver.Chrome() browser.get(“https://example.com/login”)
wait = WebDriverWait(browser, 10) username = wait.until(EC.visibility_of_element_located((By.ID, “username”))) username.send_keys(“alice”)
submit = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, “button[type=’submit’]”))) submit.click()
This is better than time.sleep(10) because it stops waiting as soon as the condition is true.
Common explicit wait conditions
The Selenium expected conditions API gives you practical building blocks:
presence_of_element_located, element exists in the DOMvisibility_of_element_located, element exists and is visibleelement_to_be_clickable, visible and enabledtext_to_be_present_in_element, useful for status messagesurl_contains, useful after navigationtitle_contains, useful for page-level assertionsinvisibility_of_element_located, useful for spinners and overlays
Example, waiting for a loading spinner to disappear:
python wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, “.spinner”))) wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “.results-table”)))
Why explicit waits reduce flaky tests
Explicit waits map to the actual app behavior. If a button is clickable only after a request finishes, wait for clickability. If a toast is supposed to appear, wait for the toast text. This makes failure messages more useful than a raw sleep timeout, because the test expresses intent.
But explicit waits are not magic
Explicit waits can still fail when your condition is too weak or too strong.
Examples:
- Waiting for visibility when the element is visible but still disabled.
- Waiting for clickability when an overlay intermittently covers the button.
- Waiting for a selector that is not stable across releases.
- Waiting for text that changes due to localization or A/B tests.
The key is to choose a condition that matches the user action you are modeling.
Selenium fluent wait, when you need more control
Fluent wait is what you reach for when polling behavior matters. In Selenium Python, you can simulate fluent wait behavior by configuring WebDriverWait with a polling interval and ignored exceptions.
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
browser = webdriver.Chrome() wait = WebDriverWait( browser, timeout=15, poll_frequency=0.5, ignored_exceptions=(NoSuchElementException, StaleElementReferenceException) )
browser.get(“https://example.com”)
price = wait.until(lambda d: d.find_element(By.CSS_SELECTOR, “.price”).text != “”)
This is useful when the page is refreshing parts of the DOM and you expect temporary lookup or reference failures.
When fluent wait is worth it
Use a fluent wait when:
- the DOM is unstable during transitions,
- the default polling interval is too slow for your case,
- you need to ignore specific transient exceptions,
- you need custom logic that is not covered by built-in expected conditions.
When fluent wait is overkill
If the built-in expected condition works, use it. A custom polling function is harder for the next engineer to read, and you have to maintain its behavior when the app changes.
Avoid mixing waits without a reason
Mixing implicit and explicit waits is one of the most common timing mistakes I see in Selenium codebases. The issue is not that the combination is always broken, it is that the interactions are not obvious.
If you use explicit waits heavily, keep the implicit wait at zero unless you have a concrete reason not to.
browser = webdriver.Chrome()
browser.implicitly_wait(0)
That makes the timing behavior more predictable. Your wait logic lives in one place instead of being spread across driver configuration and test helpers.
The wrong fix, hard sleeps
Sometimes people use time.sleep() because the test is failing and they want the fastest possible patch. That can be okay for a quick experiment, but it is usually a maintenance problem waiting to happen.
import time
time.sleep(5) submit = browser.find_element(“css selector”, “button[type=’submit’]”) submit.click()
Why this is a bad default:
- it slows down passing tests,
- it is still flaky if the app takes longer than expected,
- it does not tell you what state you were waiting for,
- it makes the suite harder to tune.
If the wait is truly about non-deterministic external behavior, such as a delayed email or a third-party callback, a sleep may be the least bad temporary workaround. But for ordinary UI readiness, use a condition.
Choosing the right wait for the job
A useful way to think about Selenium waits is to map them to the type of problem.
Use implicit wait when
- you need a small global fallback,
- you are dealing with a legacy suite and want minimal changes,
- you understand the tradeoff and have limited explicit waits.
Use explicit wait when
- the page or widget has a known ready state,
- you need readable, test-specific synchronization,
- you want the wait to reflect user-visible behavior.
Use fluent wait when
- the DOM changes rapidly during the wait,
- you need custom polling behavior,
- you need to ignore transient exceptions.
Use neither when
- the page is already stable and the action is immediately available,
- you are asserting a synchronous API response through a better layer,
- the test should fail fast when the element is missing.
The best wait is the one that matches a real application state, not a guessed delay.
Practical patterns for flaky Selenium tests
Waits are only one part of the stability story. If your suite is flaky, the root cause might be elsewhere.
1. Wait on the smallest meaningful state
Do not wait for the whole page if only one component matters.
Bad:
python wait.until(EC.title_contains(“Dashboard”))
Better, if the user is waiting on a table:
python wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “table.reports”)))
2. Prefer stable locators
A fragile locator can look like a timing problem.
Prefer data-testid or similarly stable attributes when your team controls the app.
python wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, “[data-testid=’save-profile’]”))).click()
3. Wait for the result of the action, not just the action target
If clicking “Save” triggers a toast, API update, and redirect, assert the outcome. A click alone does not prove the operation completed.
python wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “.toast-success”)))
4. Watch for stale elements
A frequent Selenium failure mode is StaleElementReferenceException, which happens when the element you found is replaced in the DOM after a re-render.
In that case, re-locate the element after the UI updates, or use a wait condition that tolerates the transition.
5. Keep waiting logic close to user intent
If every test calls a shared helper like wait_for_app_ready() but the helper means different things on different pages, the abstraction becomes a liability. It hides the state that actually matters.
A small end-to-end example
Here is a realistic flow: open a login page, submit credentials, wait for the dashboard, then verify a widget is loaded.
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
browser = webdriver.Chrome() wait = WebDriverWait(browser, 12)
browser.get(“https://example.com/login”)
wait.until(EC.visibility_of_element_located((By.ID, “email”))).send_keys(“alice@example.com”) wait.until(EC.visibility_of_element_located((By.ID, “password”))).send_keys(“secret”) wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, “button[type=’submit’]”))).click()
wait.until(EC.url_contains(“/dashboard”)) wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “[data-testid=’usage-chart’]”)))
This test is readable because each wait describes the state needed by the next step.
Debugging wait failures
When a wait fails, do not immediately increase the timeout. First ask what condition was actually missing.
Questions to ask
- Did the element never appear, or did it appear late?
- Was the locator wrong?
- Was the element hidden, disabled, or overlapped?
- Did navigation happen slower in CI than locally?
- Is a backend dependency causing the delay?
- Did the page re-render and stale the element?
Useful debugging techniques
- Capture screenshots on failure.
- Log the selector and wait condition.
- Inspect browser console errors.
- Record network activity if the app depends on API responses.
- Compare local and CI execution paths.
If a test only passes when the timeout is unusually large, that is usually a signal that the test is waiting on the wrong thing or that the app has a genuine performance issue.
Waits in CI/CD pipelines
Selenium timing issues often get worse in CI because browser startup, CPU contention, and shared environments add variability. Continuous integration is supposed to make failures repeatable, not surprising, so your waits should be designed for the slower path that CI exposes.
A few practical rules:
- make your default wait times explicit,
- keep the suite deterministic under load,
- avoid cascading waits that hide the first failure,
- collect artifacts when a timeout happens,
- separate true product latency from test synchronization problems.
A CI job that times out because the test waited for a spinner that never disappeared is much more actionable than a job that failed after random sleeps in several helper functions.
How Selenium compares to newer approaches
Selenium remains a solid choice when you need broad browser support or you already have a mature WebDriver suite. But the cost of maintaining wait logic scales with suite size. Every new page, widget, and async flow adds another place where a test can race the UI.
Playwright reduces some of this pain with auto-waiting and a tighter browser model, but Selenium still has a large installed base and a lot of real production value. The point is not that one tool eliminates timing issues, it is that the ergonomics differ.
If your organization wants to keep Selenium but reduce the amount of hand-written synchronization, a platform like Endtest vs Selenium can be a simpler alternative for teams that do not want to manually manage wait logic across a large test suite. Endtest uses agentic AI and platform-native editable steps, which can reduce the amount of custom waiting code teams need to own. That said, tool choice should still be evaluated against your workflow, not slogans.
A practical rule set I would use on a real team
If I were standardizing Selenium waits across a codebase, I would start here:
- Set implicit wait to zero, unless a legacy constraint says otherwise.
- Use explicit waits for every interaction that depends on rendered UI state.
- Use built-in expected conditions before writing custom polling logic.
- Use fluent wait only when you can explain why the defaults are not enough.
- Replace sleeps with state-based waits wherever possible.
- Treat repeated timeout failures as a product or locator problem until proven otherwise.
- Keep wait helpers small and specific, so failure messages stay readable.
That approach does not remove all flakiness, but it makes the flakiness visible and actionable.
Conclusion
Selenium waits are not about adding delay, they are about making your test synchronized with the application. Once you understand the difference between implicit wait, explicit wait, and fluent wait, the rest becomes an engineering choice: what state matters, how long are you willing to wait, and how much complexity do you want to carry in the suite.
For most teams, the practical answer is straightforward. Use explicit waits as the default, keep implicit waits minimal or disabled, reach for fluent waits only when needed, and treat every timeout as a clue about the app or the test design. That is how Selenium waits explained becomes less about memorizing APIs and more about building tests that survive real browser behavior.