Root cause #
cy.wait(<number>) encodes an assumption about timing that the network does not honor. The duration of an XHR or fetch depends on payload size, backend load, and CI contention, none of which are constant. Pick a number too small and the test asserts on the DOM before data arrives, producing an intermittent failure. Pick a number too large and every run pays the full cost whether or not the request was fast. Worse, a fixed wait masks ordering: two requests in flight may resolve in either order, and a blind timer cannot tell which.
Aliasing replaces the guess with a fact. cy.intercept(...).as('getUsers') binds the matched route to a name, and Cypress records each matching request. cy.wait('@getUsers') then blocks until that specific request has been issued and resolved, returning the interception object so you can assert on its status, body, or request payload. Because the wait is keyed to the real event, it self-adjusts to whatever latency the run actually produces — fast when the response is fast, patient when it is slow, and never racy.
Step-by-step fix #
1. Alias the interception before the action #
Register and name the route before the click or navigation that triggers it.
// Name the route so we can wait on this exact request later.
cy.intercept('GET', '/api/v1/users').as('getUsers');
cy.visit('/users');
// Blocks until the named request resolves — no fixed timer, no guessing.
cy.wait('@getUsers');
cy.get('[data-cy=user-row]').should('have.length.greaterThan', 0);
2. Assert on the interception, not just the DOM #
cy.wait yields the interception so you can verify status and payload, catching backend regressions the UI might hide.
cy.wait('@getUsers').then(({ request, response }) => {
// Asserting on the response status catches a 500 the DOM might render as "empty".
expect(response.statusCode).to.eq(200);
expect(request.headers).to.have.property('authorization');
});
3. Wait on multiple requests deterministically #
Pass an array to synchronize on several aliases regardless of resolution order.
cy.intercept('GET', '/api/v1/users').as('users');
cy.intercept('GET', '/api/v1/roles').as('roles');
cy.visit('/admin');
// Resolves only when BOTH have completed, in any order — no ordering assumptions.
cy.wait(['@users', '@roles']);
4. Wait on a specific occurrence #
When the same route fires repeatedly (pagination, polling), index the alias.
cy.intercept('GET', '/api/v1/feed*').as('feed');
cy.get('[data-cy=load-more]').click();
cy.get('[data-cy=load-more]').click();
// Waits for the SECOND feed request specifically, not just any one.
cy.wait('@feed.2');
For matcher precision that keeps these aliases from catching unintended traffic, see Cypress cy.intercept() Best Practices for Flaky Tests.
Pitfalls #
- Aliasing after the triggering action. Mitigation: always register
cy.intercept().as()before the click or visit. - Replacing fixed waits with
cy.wait('@alias')but then adding anothercy.wait(500)“just in case”. Mitigation: trust the alias and remove every numeric wait. - A matcher so broad the alias resolves on an unrelated request. Mitigation: scope method and URL tightly so the alias maps to one logical call.
- Waiting once on a route that fires multiple times. Mitigation: use indexed aliases (
@feed.2) for the specific occurrence. - Forgetting that
cy.waitonly confirms the request happened, not that rendering finished. Mitigation: follow the wait with a DOM.should()assertion.
Reliability targets #
| Target | Goal |
|---|---|
Fixed cy.wait(<ms>) calls remaining |
0 in the suite |
| Timing-related flakiness | < 0.5% of runs |
| Median spec runtime change | faster (no padded waits) |
| CI pass rate | ≥ 99.5% |
Frequently Asked Questions #
Q: Why is cy.wait('@alias') better than cy.wait(3000)?
A: The alias resolves exactly when the named request completes, so it adapts to real latency. A numeric wait either fires too early and flakes or too late and wastes time on every run.
Q: Can I assert on the request body that was sent?
A: Yes. cy.wait('@alias') yields the full interception, so you can inspect request.body, request.headers, and response to validate both directions of the call.
Q: How do I wait for several requests that resolve in any order?
A: Pass an array of aliases to cy.wait(['@a', '@b']). It completes only when all of them have resolved, with no assumption about ordering.