Understanding Cypress.io’s Built‑In Retry‑Ability for Stable UI Tests
Learn how Cypress.io’s automatic retry‑ability reduces flaky UI tests, see a concrete example, and understand the trade‑offs before enabling it in your suite.
28 Oct 2025, 03:25 UTC

Problem: flaky UI tests caused by asynchronous rendering
When a test tries to interact with an element that appears after a network request or animation, the command may run before the element is in the DOM. The test then fails with a timeout error, even though the application works correctly. Teams often counter this by adding hard‑coded waits (cy.wait) or increasing default timeouts, which makes tests slower and still brittle.
How Cypress.io’s retry‑ability works
Cypress automatically re‑executes a command and its associated assertions until they succeed or a configurable timeout expires. The mechanism is built into every Cypress command (cy.get, cy.contains, cy.click, …) and into Chai‑style assertions chained with .should or .expect. On each attempt Cypress waits a short interval (default 10 ms) before retrying, and the total time allowed is the command’s timeout (default 4000 ms). In the Command Log you will see entries labelled 'Retrying' with timestamps for each attempt.
Worked example: button that shows a spinner before revealing text
Consider a component that, when clicked, displays a spinner while fetching data and then replaces the spinner with the text 'Loaded'. A naïve test might look like this:
// cypress/e2e/spinner.cy.js
it('shows loaded text after click', () => {
cy.visit('/spinner-page')
cy.get('button').click()
// The text appears after the spinner disappears
cy.contains('Loaded').should('be.visible')
})
If the network request takes longer than the default 4 second timeout, the cy.contains command will fail before the text is rendered, producing a flaky failure. By enabling retry‑ability, Cypress will keep trying cy.contains until the text appears or the timeout is reached.
To enable retry‑ability for this test suite, add the following to cypress.json (or cypress.config.js):
{
"retries": {
"runMode": 2,
"openMode": 0
}
}
runMode controls how many times Cypress will retry a failed command during headless runs (here: 2 retries, i.e. up to three total attempts). openMode disables retries while the Test Runner is open, which keeps debugging faster.
When you run the test with the configuration above, you should observe:
- The Command Log shows a 'Retrying' entry after the first cy.contains attempt, followed by a second attempt if needed.
- If the element appears within the timeout, the test passes; if it never appears, the test fails after the final attempt with a clear timeout error.
Trade‑offs and limitations
While retry‑ability removes the need for explicit waits, it can hide genuine performance problems. If the application consistently needs more than the configured timeout to render an element, the test will still fail, but the retry attempts add extra delay each run, increasing overall suite duration. For a large suite with hundreds of tests, even a few extra milliseconds per command can accumulate to noticeable runtime growth.
Retry‑ability only applies to Cypress commands and assertions. Code executed via cy.window(), cy.task(), or raw JavaScript inside a .then callback is not retried. Teams must still handle asynchronous behavior in those contexts with explicit promises or async/await.
Finally, because each retry introduces a short pause, tests that rely on precise timing (e.g., animation frame checks) may produce false positives if the condition becomes true only after a retry.
Practical steps for adoption
Adopting retry‑ability involves a few straightforward steps:
- Add the retries block to your Cypress configuration file, choosing values that match your application’s typical latency. Start with runMode: 1 and observe the impact.
- Run the suite in headless mode and record the total execution time. Compare it to a baseline run with retries disabled (set both to 0).
- Inspect the Command Log for a few representative tests; verify that 'Retrying' entries appear only when an element is temporarily absent.
- If you notice a significant increase in runtime, consider tightening the command timeout (defaultTimeout) instead of increasing retry counts, or investigate the underlying latency in the application.
- Document the decision in your team’s testing guidelines so new members understand why explicit cy.wait calls are discouraged.
- Re‑run the suite after any change to confirm that the retry configuration still provides the desired balance between stability and speed.
Closing
Cypress.io’s built‑in retry‑ability turns many flaky UI tests into reliable checks by automatically re‑evaluating commands and assertions. The feature is easy to enable, works transparently with the existing API, and provides visible feedback in the Command Log. However, it is not a substitute for fixing slow rendering or for handling asynchronous code outside Cypress commands. By measuring runtime impact and reviewing logs, teams can adopt retry‑ability where it adds confidence without masking real performance issues.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.