Synchronization: Implicit, Explicit, Fluent, and Custom Waits: Guided Hands-On Workflow
Operate a disposable delayed-render fixture and compare immediate lookup, an intentionally bad fixed sleep, isolated implicit waiting, explicit Expected Conditions, and a custom domain condition with timing evidence.
Learning objectives
- Create a disposable loopback delayed-render fixture with deterministic timing and synthetic state.
- Measure immediate lookup failure and an intentionally brittle fixed-sleep baseline without treating either as the solution.
- Demonstrate implicit waiting in its own session and keep explicit/custom-wait sessions at an implicit wait of zero.
- Use Expected Conditions for presence/visibility and a custom condition for domain-specific application readiness.
- Record elapsed time, session identity, browser capabilities, state snapshots, exceptions, and screenshots so causality is visible.
- Choose the correct wait condition for a new scenario instead of copying a timeout value.
1. Lab contract and timing map
This lab uses a static fixture served only from
127.0.0.1. The page accepts a deterministic
?delay= query value, renders a result after that delay,
then changes the application status to ready. Nothing
calls an external API, uses a real account, or stores credentials.
The purpose is to make the race reproducible enough to measure.
The following table organizes the key choices and evidence for Lab contract and timing map. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.
| State | Before trigger | During delay | Ready |
|---|---|---|---|
#status[data-state] |
idle | loading | ready |
#result |
absent | absent | present + visible |
| Session | alive | alive | alive |
| Evidence | baseline metadata | elapsed measurement | result + screenshot/timing record |
2. Create the disposable project
The following example makes the Create the disposable project behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
mkdir selenium-waits-lab
cd selenium-waits-lab
python -m venv .venv
# Linux/macOS:
. .venv/bin/activate
# Windows PowerShell:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
mkdir site evidence
The following example makes the Create the disposable project behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python -c "import selenium; print('selenium', selenium.__version__)"
python --version
Start a supported local browser at least once if the environment has never resolved its driver. Modern Selenium normally invokes Selenium Manager automatically when no driver path is supplied.
3. Create the deterministic delayed-render fixture
The following example makes the Create the deterministic delayed-render fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Synchronization Fixture</title></head>
<body data-build="ch06-l2-v1">
<h1>Delayed result</h1>
<button id="start">Start</button>
<div id="status" data-state="idle">Idle</div>
<div id="mount"></div>
<script>
const params = new URLSearchParams(location.search);
const delay = Number(params.get('delay') || '1200');
const status = document.querySelector('#status');
const mount = document.querySelector('#mount');
document.querySelector('#start').addEventListener('click', () => {
status.dataset.state = 'loading';
status.textContent = `Loading for ${delay} ms`;
mount.replaceChildren();
setTimeout(() => {
const result = document.createElement('p');
result.id = 'result';
result.dataset.requestId = `REQ-${delay}`;
result.textContent = `Result after ${delay} ms`;
mount.append(result);
status.dataset.state = 'ready';
status.textContent = 'Ready';
}, delay);
});
</script></body></html>
Save as site/index.html, then run this in a separate
terminal:
python -m http.server 8765 --bind 127.0.0.1 --directory site
4. Baseline A: immediate lookup fails for the right reason
The following example makes the Baseline A: immediate lookup fails for the right reason behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
driver.get(URL)
assert driver.timeouts.implicit_wait == 0
driver.find_element(By.ID, "start").click()
t0 = monotonic()
try:
driver.find_element(By.ID, "result")
except NoSuchElementException as exc:
print(type(exc).__name__, "after", round(monotonic() - t0, 3), "s")
print("status", driver.find_element(By.ID, "status").get_dom_attribute("data-state"))
finally:
driver.quit()
Expected observation: the lookup fails almost immediately while
status is loading. That is not Selenium being
unreliable; with an implicit wait of zero, the command accurately
reports that the node does not exist yet.
5. Baseline B: a fixed sleep guesses instead of observing
The following example makes the Baseline B: a fixed sleep guesses instead of observing behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# INTENTIONALLY BRITTLE DEMONSTRATION — do not use as the final solution.
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
driver.get(URL)
driver.find_element(By.ID, "start").click()
time.sleep(0.8) # guessed delay: intentionally shorter than fixture transition
try:
print(driver.find_element(By.ID, "result").text)
except NoSuchElementException:
print("sleep expired but application is still", driver.find_element(By.ID, "status").text)
finally:
driver.quit()
The failure is deterministic here because the fixture needs 1.2 seconds and the test guesses 0.8 seconds. Increasing the sleep to 2 seconds would hide the race for this one case while wasting time whenever the application finishes sooner.
6. Isolated implicit-wait demonstration
Use a fresh session so this global setting cannot contaminate the explicit-wait examples that follow.
from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By
URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
driver.implicitly_wait(2)
driver.get(URL)
driver.find_element(By.ID, "start").click()
t0 = monotonic()
result = driver.find_element(By.ID, "result")
print("found after", round(monotonic() - t0, 3), "s")
print(result.text)
finally:
driver.quit()
The lookup returns soon after the node appears; it does not necessarily consume the full two seconds. This demonstrates the mechanism, but the global policy still cannot express “visible,” “ready,” or the expected request identity.
7. Explicit wait: bind the wait to the next required state
The following example makes the Explicit wait: bind the wait to the next required state behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from time import monotonic
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
URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
assert driver.timeouts.implicit_wait == 0
driver.get(URL)
driver.find_element(By.ID, "start").click()
wait = WebDriverWait(driver, 2.5)
t0 = monotonic()
result = wait.until(EC.visibility_of_element_located((By.ID, "result")))
elapsed = monotonic() - t0
assert result.text == "Result after 1200 ms"
print("visible after", round(elapsed, 3), "s")
finally:
driver.quit()
The timeout is a ceiling, not a sleep. The relevant contract is
visibility of #result, because the next line reads what
a user can see. If the condition never becomes true before 2.5
seconds, WebDriverWait raises
TimeoutException; preserve the page state before
changing the budget.
8. Custom condition: express domain readiness, not just DOM presence
The following example makes the Custom condition: express domain readiness, not just DOM presence behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from time import monotonic
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
class RequestReady:
def __init__(self, request_id):
self.request_id = request_id
def __call__(self, driver):
status = driver.find_element(By.ID, "status")
if status.get_dom_attribute("data-state") != "ready":
return False
result = driver.find_element(By.ID, "result")
return result if result.get_dom_attribute("data-request-id") == self.request_id else False
URL = "http://127.0.0.1:8765/?delay=1200"
driver = webdriver.Chrome()
try:
assert driver.timeouts.implicit_wait == 0
driver.get(URL)
driver.find_element(By.ID, "start").click()
t0 = monotonic()
result = WebDriverWait(driver, 2.5, poll_frequency=0.2).until(RequestReady("REQ-1200"))
print(result.text, "after", round(monotonic() - t0, 3), "s")
finally:
driver.quit()
This wait proves two facts: the application phase is ready and the visible result belongs to the expected request. That is stronger than “some node named result exists.”
9. Record causality, not only pass/fail
The following example makes the Record causality, not only pass/fail behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
evidence = {
"session_id": driver.session_id,
"browser": driver.capabilities.get("browserName"),
"browser_version": driver.capabilities.get("browserVersion"),
"implicit_wait": driver.timeouts.implicit_wait,
"url": driver.current_url,
"status": driver.find_element(By.ID, "status").get_dom_attribute("data-state"),
"request_id": result.get_dom_attribute("data-request-id"),
"elapsed_seconds": round(elapsed, 3),
}
print(evidence)
On failure, capture the same state plus a screenshot and the first exception before teardown. The evidence should tell you whether the condition was never met, the wrong page loaded, the request identity changed, or the browser/session itself failed.
10. Challenge: choose the condition
A panel is inserted immediately, starts hidden, becomes visible after data arrives, and its Save button becomes enabled 300 ms later. Your next command must click Save. Which contract is strongest: presence of the panel, visibility of the panel, text in the panel, or clickability/enabled state of Save? Explain which state the next command actually requires and what additional application evidence you would assert after the click.
Knowledge check
Why is the fixed-sleep example included even though fixed sleeps are discouraged?
It creates a measurable brittle baseline so the learner can compare guessed wall-clock delay with condition-based synchronization; it is explicitly not the final strategy.
Why is the implicit-wait demonstration run in a separate session?
Implicit wait is session-global. Isolating it prevents later explicit/custom examples from accidentally mixing wait strategies.
What does visibility_of_element_located add beyond presence?
It requires a located element that Selenium considers visible, matching a next step that needs visible content.
Why does the custom RequestReady condition inspect request identity?
It prevents the test from accepting a stale or unrelated result merely because a result node exists.
If a 2.5-second explicit wait succeeds after 1.2 seconds, how long should the next command wait?
It proceeds immediately after the condition succeeds; the remaining timeout budget is not consumed.
Official references and version notes
- Selenium 4.47 release — current pinned Selenium release baseline for this chapter.
- Waiting Strategies — current Selenium guidance on page-load readiness, implicit waits, explicit waits, and the warning against mixing implicit and explicit waits.
- Waiting with Expected Conditions — common explicit-wait conditions such as presence, visibility, stale state, text, and title checks.
-
Python WebDriverWait API 4.47.0
— timeout, poll frequency, ignored exceptions,
until, anduntil_not. - Python Timeouts API 4.47.0 — implicit, page-load, and script timeout concepts.
Version-sensitive behavior was rechecked against Selenium primary
documentation on 2026-08-28. Mandatory examples
pin Selenium Python 4.47.0 on Python 3.10+, use a
supported locally installed Chromium-family browser with ordinary
Selenium Manager resolution, and target only loopback fixtures.
Python has no separate Java-style FluentWait class:
configurable fluent behavior is provided by
WebDriverWait(timeout, poll_frequency,
ignored_exceptions). Grid, browser clouds, enterprise identity, and BiDi are not
required in this chapter. The intentionally brittle sleep is
confined to a comparison baseline. The stable workflow uses
bounded observable conditions and keeps explicit/custom sessions
at an implicit wait of zero.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.