Cypress Retry-Ability: Replacing cy.wait() with Automatic Command Retries
Cypress retries commands and assertions automatically. Learn how to replace arbitrary cy.wait() calls with retryable queries and assertions, and where the mechanism stops working.
10 Aug 2026, 09:42 UTC

Flaky Cypress tests usually share a root cause: the test assumes an element is ready before the application has finished rendering it. The quick fix is cy.wait(5000), but that adds dead time to every run and hides the real issue. Cypress already ships with automatic waiting and retry-ability. When you lean on that mechanism, you can delete arbitrary waits and keep the suite both fast and honest.
The core idea is simple: every Cypress command and assertion retries until it succeeds or hits a timeout. You do not need to tell Cypress to wait for a button. You need to issue a query and an assertion that describe the ready state, then let the runner poll.
How DOM queries retry
Commands such as cy.get() and cy.contains() do not resolve on a single attempt. Cypress re-queries the DOM in a retry loop until the selector matches or the defaultCommandTimeout expires. In most Cypress versions the default is 4 seconds, though you can override it globally or per test. If you call cy.get('#submit') before the element exists, Cypress keeps looking until it appears or the timeout fires.
Assertions retry with the command
Assertions attached with .should() or .and() are not one-shot checks. They re-run on every retry cycle. Writing cy.get('#status').should('have.text', 'Complete') tells Cypress to keep checking the text until it matches. The query locates the element; the assertion defines when it is ready. Pairing the two is the idiomatic pattern.
Worked example: a button that appears late
Consider a dashboard where a submit button is injected two seconds after page load because of a client-side timer or a pending API response. A brittle test inserts cy.wait(2000). A resilient test trusts the retry loop.
// cypress/e2e/delayed-button.cy.js
describe('Delayed button', () => {
it('clicks a button that appears after an async update', () => {
cy.visit('/dashboard');
// No cy.wait() needed. Cypress retries get() until the button exists,
// then retries the click until the element is actionable.
cy.get('[data-cy=delayed-submit]')
.should('be.visible')
.click();
cy.get('[data-cy=result]').should('contain', 'Submitted');
});
});
Running and checking the example
- Where: Run from the project root in a terminal.
- Command:
npx cypress run --spec cypress/e2e/delayed-button.cy.js - Permissions: Standard user access is fine; Cypress needs write access for videos and screenshots if those are enabled.
- Placeholders: Replace
[data-cy=delayed-submit]and[data-cy=result]with selectors that match your application. - Expected check: Open the Command Log in the Cypress Runner. The
get()command should show a retry indicator before turning green. Total test time should be close to the real delay plus minimal overhead. - Risk: If you lower
defaultCommandTimeoutbelow the delay, the command will fail with a timeout. Use that boundary to understand the limit, not as a permanent setting.
To change the global timeout, edit your Cypress configuration. In Cypress 10 and later this is cypress.config.js:
// cypress.config.js
module.exports = {
e2e: {
defaultCommandTimeout: 4000,
},
};
Older projects using cypress.json accept the same key at the top level. Keep the value as low as your slowest legitimate interaction allows.
Where retry-ability stops
Automatic retry only applies to Cypress commands and assertions chained inside the test. It does not cover arbitrary JavaScript. A raw Promise, a setTimeout callback, or a fetch call written outside a Cypress command will not be retried. The runner will either move on before your async code finishes or fail unpredictably. To bring custom async work into the retry world, wrap promise-like values with cy.wrap(), or move the logic into a cy.task() plugin. Even then, the task runs once; retries happen at the command boundary, not inside the task itself.
A related risk is complacency. If you raise defaultCommandTimeout to 30 seconds because the app feels slow, your suite will pass while the user experience degrades. The Command Log is your diagnostic: if a get() spends 25 seconds retrying, you have found a performance bug, not a stable test.
Practical takeaways
Audit your specs for cy.wait() calls with numeric arguments. Replace each one with a cy.get() or cy.contains() query plus a .should() assertion that describes the ready state. Leave requestTimeout and responseTimeout at their defaults unless you are explicitly debugging network interception; those settings govern how long cy.intercept() waits for matching traffic, not how long the DOM takes to render. Finally, watch the Command Log during a run. Quick retries that settle in under a second are healthy. Long, grinding retries are a signal to fix the application, not the test.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.