Why Hard Waits Make Test Automation Unreliable
Fixed sleeps make UI tests slower and still leave race conditions. Learn how to wait for the state your next step actually needs in Selenium, Cypress, and Playwright.
A hard wait pauses for a fixed duration without checking whether the application is ready. If the page takes longer, the test continues too early and can fail; if it takes less time, the test wastes the remaining delay. Replace sleeps used for UI synchronization with a wait for the specific condition the next step requires.
For example, wait for a button to become visible or enabled before clicking it, or for expected text to appear before asserting it. Frameworks provide different mechanisms for doing this: Selenium has explicit waits, Cypress retries queries and assertions, and Playwright provides auto-waiting actions and web-first assertions.
Why fixed waits cause flaky tests
Dynamic pages often change after the browser has finished its initial document load. JavaScript may still fetch data, update components, or enable controls. Selenium’s documentation explains that a command can race ahead of those changes, and that readyState alone does not guarantee that the application is ready for the next interaction. It identifies race conditions as a primary cause of flaky tests. Selenium: Waiting Strategies
A sleep encodes an assumption about duration, not a readiness condition. A short guess fails on slower runs; a long guess makes every run wait, even when the page is ready sooner. Adding more delay may hide a timing problem temporarily without establishing that the expected behavior occurred.
Cypress recommends using an explicit assertion that it can retry when tempted to call cy.wait(number). Its performance guide also recommends adjusting the relevant timeout for a known slow operation instead of adding a fixed pause. Cypress: Unnecessary Waiting · Cypress: Optimizing test performance
Choose the condition the next step needs
Before adding a wait, identify what must be true for the next action or assertion to make sense. Wait for that signal as close as possible to where it is needed.
| Next step | Useful condition |
|---|---|
| Click a newly shown control | The control is visible and actionable |
| Read a result | The expected text or content is present |
| Submit a form | The submit control is enabled and the form is valid |
| Show server-backed results | The relevant request has completed, followed by an assertion on the rendered result |
| Navigate to a new view | A locator or page-specific state identifies the new view |
A timeout is an upper bound for observing a condition. When the condition becomes true earlier, a condition-based wait can proceed immediately. Set the timeout for the operation that is genuinely slow; do not use a long sleep to make all runs pay for the slowest case.
Selenium: use explicit waits for UI state
Selenium supports implicit and explicit waits. An explicit wait polls for a particular condition and proceeds when it becomes true or raises a timeout error when the limit is reached. Avoid mixing implicit and explicit waits: Selenium warns the combination can produce unpredictable total wait times. Selenium: Waiting Strategies
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
wait = WebDriverWait(driver, 10)
# Wait for the control required by the next action.
button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button#continue"))
)
button.click()
# Then wait for the resulting page state.
message = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "#success-message"))
)
assert "Complete" in message.text
finally:
driver.quit()
Remove the accidental leading space before driver = webdriver.Chrome() and the indented lines if pasting into a top-level Python file; the runnable version with normal top-level indentation is below:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
wait = WebDriverWait(driver, 10)
button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button#continue"))
)
button.click()
message = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "#success-message"))
)
assert "Complete" in message.text
finally:
driver.quit()
Use the condition that matches the requirement. Presence means the element exists in the DOM; visibility means it is displayed; clickability is a stronger prerequisite for a click. None of these proves that unrelated business logic succeeded, so assert the resulting state as well.
If you configure an implicit wait, understand that it applies broadly to element searches. For tests that need precise synchronization, explicit waits make the condition and timeout visible at the call site. Do not combine the two casually.
Cypress: rely on retryable queries and assertions
Cypress retries linked queries and assertions until they pass or time out. Use a query such as cy.get() followed by an assertion about visibility, text, or state. Cypress actions also wait for actionable elements according to its documented behavior. Cypress: Retry-ability · Cypress: Interacting with Elements
describe('checkout', () => {
it('continues after the confirmation control is ready', () => {
cy.visit('https://example.com/checkout')
cy.get('button#continue')
.should('be.visible')
.and('be.enabled')
.click()
cy.get('#success-message')
.should('be.visible')
.and('contain.text', 'Complete')
})
})
When a particular request is the synchronization point, alias it and wait for that alias. Then assert the user-visible result separately: a response completing does not establish that the expected content rendered correctly. Cypress documents this pattern in its examples. Cypress: Network Requests
describe('search', () => {
it('shows results from the search request', () => {
cy.intercept('GET', '/api/search*').as('search')
cy.visit('https://example.com/search?q=widgets')
cy.wait('@search')
.its('response.statusCode')
.should('eq', 200)
cy.get('[data-testid="search-results"]')
.should('be.visible')
.and('contain.text', 'Widget')
})
})
For a known slow request or command, set an appropriate operation-specific timeout where Cypress supports it. Cypress’s performance documentation describes a four-second default command timeout, but defaults are framework-specific and can vary; consult the current configuration documentation rather than treating that value as universal.
Playwright: use auto-waiting and web-first assertions
Playwright’s locator actions wait for relevant actionability conditions before acting. Its web-first assertions retry until the expected state is observed or the assertion times out. Prefer these built-in waits over sleeping between actions. Playwright: Auto-waiting · Playwright: Writing tests
import { test, expect } from '@playwright/test';
test('continues when the checkout result is ready', async ({ page }) => {
await page.goto('https://example.com/checkout');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.locator('#success-message')).toContainText('Complete');
});
The locator-based click waits for the button’s actionability requirements. The assertion waits for the resulting UI. If the page exposes a more specific locator or state, use it; broad selectors can match the wrong element or become ambiguous.
Synchronize on network activity only when it is the right signal
Waiting for a specific request is useful when that request is the event the test needs to coordinate with—for example, a search request triggered by navigation. It is not a substitute for checking the rendered outcome. A completed request can return an error, produce unexpected data, or be followed by a rendering problem.
- Identify the request that the action should trigger.
- Register the wait or alias before triggering the action, so a fast response cannot be missed.
- Assert relevant response details when they matter, such as status or data.
- Assert the resulting user-visible state separately.
A generic “network idle” condition is not always the right definition of readiness: applications may poll, stream, load analytics, or make background requests that continue after the target UI is ready. Prefer a specific request or UI condition when possible.
Timeouts, retries, and reliability
- Choose a meaningful timeout. It should cover expected slow behavior while still failing within a useful time when the condition never occurs.
- Keep the condition specific. A wait for a page-wide spinner to disappear may pass before the particular control is ready; wait for the control or result the next step needs.
- Keep failure informative. Use clear locators and assertions so a timeout identifies the missing state.
- Do not mask defects with retries. A retry can help with transient infrastructure, but it should not replace a deterministic readiness condition or a correct assertion.
- Measure suite time. Replacing long fixed delays with conditions that proceed as soon as they pass can reduce unnecessary waiting, while avoiding a claimed fixed speedup that depends on the application and test suite.
Framework defaults and timeout scopes differ. Selenium explicit-wait timeouts apply to the wait instance; Cypress has command and configuration timeouts; Playwright has test, action, and assertion timeout settings. Check the framework documentation for the version and configuration in use.
When an elapsed-time wait is actually required
Some tests intentionally model elapsed time, such as checking behavior after a debounce interval or a scheduled transition. Prefer controlling or advancing a test clock when the framework and application permit it. If no observable signal or controllable clock is available, a narrowly scoped delay may be necessary, but it should represent the behavior being tested rather than stand in for general page readiness. Keep the delay minimal and follow it with an assertion of the expected result.
Troubleshooting hard-wait replacements
| Symptom | Likely cause | Fix |
|---|---|---|
| Explicit wait times out even though the element seems present | The condition may require visibility or clickability while the element is hidden, covered, or disabled; the locator may also select a different match | Inspect the matched element and choose the correct locator and condition; assert the state required by the action |
| Test passes after a request wait but the page is wrong | Request completion was treated as proof of successful rendering | Check response details if relevant, then assert the actual UI content |
| Cypress wait does not prevent a flaky assertion | The alias may target the wrong request or the UI update may occur after the response | Verify the intercept pattern and add a retryable assertion for the resulting UI |
| Waits take much longer than expected in Selenium | Implicit and explicit waits may be combined, multiplying observed wait time | Remove the mixed configuration and use a consistent wait strategy |
| Playwright click times out | The locator may match multiple elements, the control may remain disabled, or another element may obstruct it | Use a more specific locator and inspect the page state and actionability diagnostics |
| Condition passes but the next step still fails | The condition did not express the true prerequisite, or the application changed again before the action | Wait for a stronger, stable condition tied to the next step and assert the outcome |
Performance, reliability, and cost
Hard waits impose their full configured delay each time they run, so many sleeps can add substantial suite time. Condition-based waits can finish as soon as their conditions pass, while still allowing a bounded wait for slower runs. The tradeoff is that vague conditions, excessive timeouts, and fragile selectors can still lead to slow or unreliable tests.
The relevant cost is usually developer and CI time rather than a special framework fee: unnecessary waiting consumes runner time, while flaky tests consume investigation and rerun time. There is no single speedup or reliability percentage that applies across applications; the outcome depends on the waits, conditions, and environment in the suite.
Or skip the browser setup
If you need a screenshot of a page to inspect or document a state, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request can return PNG, JPEG, WebP, or PDF; its API documentation lists the supported parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners are accepted and removed before capture; newsletter popups and chat widgets are also removed. Each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers report the page verdict and whether the request was billed.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Does a longer wait make a flaky test reliable?
It may reduce failures caused by one short timing guess, but it still does not prove the required state occurred and adds its full delay to each run.
Should I wait for the whole page to finish loading?
Only if whole-page completion is the actual requirement. For most interactions, a specific control or rendered result is a more useful readiness signal.
Is waiting for a request enough?
No. It confirms the request reached the wait’s completion point. Assert the user-visible outcome too when that is what the test is meant to verify.


