Why You Should Avoid Sleep in End-to-End Tests
Fixed sleeps guess when asynchronous UI work will finish. Replace them with waits for the state your test actually needs, and keep time-based waits for time-based behavior.
A fixed sleep does not tell an end-to-end test that an action finished. It only delays the next test step for a chosen amount of time. If the page takes longer, the test can race ahead and fail; if it finishes sooner, the test wastes time. After an action, wait for the result the next step depends on: a visible element, expected text, a specific response, or a framework assertion that retries until it passes or times out.
This is advice against arbitrary sleeps used as a readiness shortcut, not a ban on testing time. If a debounce, timer, or polling interval is part of the behavior, test that timing deliberately.
1. Why fixed sleeps make tests slower and less reliable
Browser tests coordinate test code with work happening in the page and on the server. A click may start JavaScript updates, network requests, rendering, or several of these at once. Their completion time can vary with network delays and server load. A sleep observes none of that work; it assumes the chosen duration was enough.
- Too short: the next assertion or interaction runs before the page is ready. The test fails intermittently, or proceeds in an invalid state.
- Too long: the test waits after the page is already ready. That time adds no confidence.
- Wrong signal: even a long sleep cannot prove the state the test cares about. It can hide a slow or broken transition until a later assertion fails.
The WEFix paper describes nondeterministic ordering between test code and client-side code as a source of UI test flakiness. In its evaluation across 122 flaky tests from seven projects, its approach had 1.25× average project-level runtime overhead, compared with 3.7× for a two-second wait strategy. Those are study-specific results, not a runtime prediction for every suite. WEFix paper.
A separate study examined 49 reproducible flaky tests from 26 open-source projects. Developers addressed asynchronous-wait failures by adapting wait time in 31 cases, about 63%, including cases where the underlying cause was elsewhere. The authors reported an average execution-time reduction of 11.1%, or 20.2% with dynamic tuning, for their suggested waits against developer-written fixes in the evaluated cases. These figures describe that study’s sample and comparison, not a guarantee for another test suite. Time-based Repair for Asynchronous Wait Flaky Tests in Web Testing.
2. Replace the sleep with the condition the next step needs
- Identify the action that starts asynchronous work, such as submitting a form or opening a dialog.
- Decide what must be true before the test can safely continue. Prefer the user-visible outcome, such as a confirmation message, over a loosely related event.
- Use the framework’s retrying assertion or explicit event wait for that condition.
- Keep a bounded timeout suitable for the application. If the condition repeatedly times out, investigate the transition and the chosen signal instead of increasing every wait.
For example, replace “click, sleep three seconds, then check for a modal” with “click, then assert that the modal is visible.” A condition-based wait can still be wrong if it watches an element that exists before it is ready, an unrelated request, or generic network idleness that does not match the application. Assert the meaningful outcome whenever possible.
Cypress: let assertions retry
Cypress recommends explicit retryable assertions instead of numeric waits. Its performance guidance says, “If you find yourself reaching for cy.wait(number), the right fix is almost always to add an explicit assertion that Cypress can retry.” Its example notes that a fixed three-second wait wastes time when a modal appears after 200 ms. Cypress: Optimizing test performance.
describe('checkout confirmation', () => {
it('shows the confirmation after checkout', () => {
cy.visit('/checkout');
cy.get('[data-testid="place-order"]').click();
// Cypress retries this query and assertion until it passes or times out.
cy.get('[data-testid="order-confirmation"]')
.should('be.visible')
.and('contain.text', 'Order placed');
});
});
When the next step specifically depends on a request, alias and wait for that request, then still assert the page outcome that matters:
describe('search results', () => {
it('renders results for the submitted query', () => {
cy.intercept('GET', '/api/search*').as('search');
cy.visit('/search');
cy.get('[name="q"]').type('camera{enter}');
cy.wait('@search').its('response.statusCode').should('eq', 200);
cy.get('[data-testid="search-results"]')
.should('be.visible')
.and('contain.text', 'camera');
});
});
The request wait is useful here because the test explicitly depends on the search response. It does not replace the UI assertion. Cypress best practices also note that arbitrary waits are almost never needed and that an ESLint rule flags cy.wait(<number>). Cypress best practices.
Playwright: use locator assertions and actionability waits
Playwright automatically waits for actionability checks before performing actions, and its asynchronous expect matchers retry until the expected condition is met. Playwright: Writing tests.
import { test, expect } from '@playwright/test';
test('checkout displays its confirmation', async ({ page }) => {
await page.goto('/checkout');
await page.getByTestId('place-order').click();
await expect(page.getByTestId('order-confirmation'))
.toBeVisible();
await expect(page.getByTestId('order-confirmation'))
.toContainText('Order placed');
});
If a specific response is the dependency, wait for it alongside the triggering action. Then assert the visible result:
import { test, expect } from '@playwright/test';
test('search renders the response', async ({ page }) => {
await page.goto('/search');
await page.getByLabel('Search').fill('camera');
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/search') && response.status() === 200
);
await page.getByRole('button', { name: 'Search' }).click();
await responsePromise;
await expect(page.getByTestId('search-results'))
.toContainText('camera');
});
Register the response wait before the action that triggers the request so the test cannot miss a fast response. Playwright marks page.waitForTimeout as discouraged; its API documentation says production tests that wait for time are inherently flaky and recommends signals such as network events or visible selectors. Playwright Page API: waitForTimeout.
Other frameworks
Use the framework’s own condition or retrying assertion APIs, and check its official documentation for exact behavior. Do not assume that every framework retries the same commands, re-resolves selectors the same way, or treats timeouts identically. Compare the specific features your test needs: action auto-waiting, assertion retries, locator behavior, event observation, timeout configuration, and failure diagnostics.
3. When waiting for time is the test
Elapsed time belongs in the test when it is part of the requirement: for example, a debounce interval, timer, delayed notification, or polling cadence. Make the time relationship explicit and local to that behavior. Avoid using a broad sleep as a substitute for observing UI readiness.
For Cypress timer-driven code, control the clock and advance it deliberately instead of waiting in real time:
describe('search debounce', () => {
it('sends the query after the debounce interval', () => {
cy.clock();
cy.intercept('GET', '/api/search*').as('search');
cy.visit('/search');
cy.get('[name="q"]').type('camera');
cy.tick(300);
cy.wait('@search');
cy.get('[data-testid="search-results"]').should('be.visible');
});
});
The example assumes the application’s debounce contract is 300 ms; use the interval specified by the application. Cypress clock controls let a test advance timers without waiting for that duration in real time. For other frameworks, use their documented fake-timer facilities where appropriate.
4. Timeouts, retries, and failure diagnosis
A timeout is a limit on how long a condition may take to become true; it is not a delay the test must always pay. Set it at the narrowest relevant scope supported by your framework, using a value consistent with the application and environment. Avoid raising global timeouts to cover one slow transition: that makes unrelated failures take longer to report and can obscure the source of the problem.
- Choose a relevant assertion: verify the state users need, not an intermediate signal that may occur too early.
- Keep waits near their cause: the action and the condition it triggers should be easy to associate in the test.
- Use network waits selectively: wait for a response only when it is a meaningful dependency. A response can arrive before rendering finishes.
- Read timeout diagnostics: inspect which condition failed, the page state, and any relevant request or browser logs before changing timeout values.
- Preserve valuable end-to-end coverage: Cypress describes end-to-end tests as the slowest full-stack layer and recommends reserving them for critical user journeys. Avoidable waits are a reason to improve synchronization, not to remove useful coverage. Cypress test performance guidance.
5. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The test passes locally but fails intermittently in CI. | A fixed sleep is shorter than some runs’ page or network work, or the test observes an unrelated signal. | Replace the sleep with a retrying assertion on the required outcome or a wait for the specific event the next step depends on. Inspect CI failure artifacts and request logs. |
| A retryable assertion times out even though the element appears. | The selector may be wrong or ambiguous, the element may not reach the asserted state, or it may appear only after the configured timeout. | Check the selector and actual state in the failure output. Assert the right readiness condition and diagnose slow or failed application work before extending a scoped timeout. |
| The test sees a response, but the page is still incomplete. | Network completion and UI rendering are separate events. | After the response wait, assert the rendered result that the next step needs. |
| A response wait sometimes misses a fast request. | The listener was registered after the action triggered the request. | Create the response promise or route alias before clicking or submitting. |
| Waiting for network idle hangs or passes at the wrong time. | The application may keep background connections open, or network idleness may not correspond to the UI state. | Wait for a relevant response or visible application outcome instead of generic network idleness. |
| The test still flakes after replacing a sleep. | The chosen condition may be unrelated, may become true too early, or another race may exist. | Identify the precise prerequisite for the next step and observe that state. Check for competing requests, stale data, and application errors. |
| A timing test is slow or depends on wall-clock scheduling. | The test waits in real time for behavior that can be driven by a controlled clock. | Use the framework’s documented fake-clock or timer controls when compatible with the behavior being tested. |
6. Performance, reliability, and cost
Removing unnecessary fixed delays reduces avoidable suite time, especially when a delay appears in many tests. It can also make a test respond sooner on fast runs while still waiting for slower valid runs, up to its timeout. Condition-based synchronization improves reliability only when the condition is correct; retries cannot repair a bad selector, failed application behavior, or an unrelated signal.
There is no universal speedup to expect. The WEFix and TRaf figures above come from specific research datasets and approaches. Measure your own suite before claiming an improvement. Cypress notes that end-to-end tests exercise the full stack and are the slowest testing layer, which is why avoiding needless waits matters while retaining tests for important user journeys.
7. Or skip the browser setup
If you need a screenshot artifact of a page while investigating browser test behavior, ScreenshotNeo provides a website screenshot API and MCP server. A screenshot can help inspect a rendered page, but it does not replace synchronizing an end-to-end test on the condition that test needs.
One GET request returns an image or PDF. See the ScreenshotNeo API docs for options and configuration.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, and failed loads are never billed. Responses identify page verdict and billing status in headers.
- An MCP server lets AI agents use screenshot, page-info, and PDF-capture tools.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
8. Frequently asked questions
Are fixed waits always wrong in end-to-end tests?
No. They are appropriate when elapsed time is itself the behavior under test or a documented external constraint makes a specific delay part of setup. Keep that wait purposeful and local.
Should I wait for the network or for the page?
Wait for the condition the next step depends on. A specific response is useful when the response itself matters, but assert the rendered state if the test depends on what users see.
Does a larger timeout make a flaky test reliable?
It can accommodate a legitimately slower condition, but it does not correct an irrelevant signal or a failed transition. Diagnose the failing condition before changing its timeout.
Can I use a screenshot to prove an end-to-end test is ready?
A screenshot records what a page looked like at capture time. The test should still wait for an application condition that represents readiness for its next action.


