QA How-To
How to Use Cypress retry-ability (2026)
Master cypress retry-ability with linked queries, assertions, timeouts, network waits, debugging patterns, and practical examples for stable tests in 2026.
25 min read | 3,546 words
TL;DR
Cypress retry-ability automatically reruns linked queries and assertions until the expected state appears or a timeout expires. Keep actions outside retryable callbacks, query the post-action state again, and synchronize with observable UI or network signals instead of fixed waits.
Key Takeaways
- Cypress retries linked queries and assertions from the top of the query chain until they pass or time out.
- Action commands execute once after Cypress satisfies their built-in actionability checks.
- Place actions at clear chain boundaries, then start a fresh query for the resulting state.
- Use retryable assertions for UI state and aliased network waits for known request boundaries.
- Put only idempotent assertion logic inside a should callback because Cypress may invoke it many times.
- Override timeouts locally around measured slow boundaries instead of raising the global timeout blindly.
- Whole-test retries are a separate diagnostic and recovery feature, not a substitute for correct synchronization.
Cypress retry-ability is the built-in mechanism that keeps querying an application and reevaluating assertions while the UI changes. In practical terms, cy.get('[data-cy=toast]').should('be.visible') waits for the toast without a hard-coded sleep, then continues as soon as the assertion passes.
This behavior is more precise than saying that Cypress retries every command. Queries link together and retry, assertions participate in that retry loop, and commands with side effects usually execute once. Understanding those boundaries is the difference between a test that naturally follows an asynchronous interface and one that occasionally observes stale or intermediate state.
This guide explains the 2026 behavior, shows safe TypeScript patterns, distinguishes command retry-ability from test retries, and gives a debugging workflow for real suites. The examples assume stable data-cy attributes and ordinary Cypress end-to-end configuration.
TL;DR
| Operation | Retried automatically? | Recommended use |
|---|---|---|
cy.get(), .find(), .contains() |
Yes, as linked queries | Locate state that may appear later |
.should() and .and() |
Yes | Express the observable condition |
.click(), .type(), .check() |
Actionability is retried, action executes once | Change state at the end of a query chain |
.then() callback |
No | Work with a resolved subject or run one-time logic |
cy.wait('@alias') |
Waits for a matching request and response | Synchronize with a known network boundary |
cy.wait(2000) |
Sleeps once | Avoid for application synchronization |
| Configured test retry | Reruns the test attempt | Detect or recover from attempt-level failure |
1. What Cypress retry-ability Means
Cypress commands enter a managed queue. When execution reaches a query and a later assertion fails, Cypress does not immediately fail the test. It repeats the linked query chain and assertion until the assertion succeeds or the applicable timeout expires. This is Cypress automatic waiting expressed through the expected result rather than an estimated delay.
Consider a page that adds an order row after a background request completes:
cy.get('[data-cy=orders-table]')
.find('[data-cy=order-row]')
.should('have.length', 1)
If the table exists but the row has not rendered, the length assertion fails temporarily. Cypress runs get, find, and the assertion again. It does not keep asserting against one frozen jQuery object. When the row appears, the chain passes immediately.
Three categories explain most behavior:
- Queries read application state and can link into a retryable chain.
- Assertions are queries with reporting behavior and are reevaluated.
- Non-query commands execute once, although action commands have built-in checks that query until the target is actionable.
The timeout is a maximum, not a mandatory wait. A condition that becomes true in 120 milliseconds continues at roughly that point even when the timeout is 10 seconds. That makes condition-based waiting both faster and more meaningful than sleeping for the worst case.
Retry-ability does not make an unsafe test deterministic by itself. The test still needs a stable starting state, controlled data, unambiguous selectors, and an assertion that describes the user-visible outcome.
2. How Cypress retry-ability Replays Linked Queries
A linked query chain is the core mental model. When its assertion fails, Cypress starts again from the first query that belongs to that chain. This matters when each query depends on a changing DOM.
cy.get('[data-cy=cart]')
.find('[data-cy=line-item]')
.filter('[data-status=ready]')
.should('have.length', 3)
Suppose React replaces the cart subtree after receiving inventory data. A one-time element reference could point at the detached old subtree. Cypress instead repeats the linked lookup and obtains the current elements. The assertion therefore evaluates live application state.
Queries are linked only until a non-query command creates a boundary. In this test, Cypress finds an actionable button, clicks once, and then the new cy.get() begins a separate retryable chain:
cy.get('[data-cy=save]').should('be.enabled').click()
cy.get('[data-cy=status]')
.should('be.visible')
.and('have.text', 'Saved')
This layout communicates two facts: the click is the single state-changing event, and the status assertion observes its result. It also allows a framework rerender between those phases without carrying an old subject forward.
Multiple .should() or .and() assertions can share a chain. Cypress repeats the chain when any current assertion fails. Keep the assertions related to one state so the eventual failure tells a coherent story. If later checks concern a different component or state transition, begin another chain.
For a deeper look at subject changes and one-time callbacks, see Cypress cy.wrap and cy.then patterns.
3. Queries, Assertions, Actions, and Other Commands
Not every item shown in the Cypress Command Log has the same retry contract. Classifying the operation before debugging prevents incorrect fixes.
| Category | Common examples | Behavior | Main risk |
|---|---|---|---|
| Query | get, find, contains, location |
Links and retries with assertions | Ambiguous selector or wrong scope |
| Assertion | should, and |
Repeats until all current expectations pass | Side effects in callback |
| Action command | click, type, select, check |
Waits for actionability, performs action once | Chaining through a rerender |
| Network command | request, aliased wait |
Has command-specific waiting and timeout rules | Matching the wrong request |
| One-time callback | then |
Runs once after prior command resolves | Expecting callback assertions to retry |
| Fixed delay | wait(number) |
Pauses for the full duration | Slow and still race-prone |
An action such as .click() includes implicit actionability checks. Cypress queries until the element exists, is visible, is not disabled, is not covered, is not animating beyond configured limits, and can receive the action. Those checks can repeat. The actual click is not repeatedly fired simply because a later assertion fails. Repeating side effects could submit two payments or delete two records, so the boundary is intentional.
Some non-DOM commands have their own waiting semantics. cy.visit() waits for the page load event within pageLoadTimeout. cy.wait('@saveOrder') waits for an intercepted request and response using request and response timeout settings. These are useful synchronizers, but they are not linked DOM queries.
When a failure says an element never became actionable, inspect the reason in the Command Log. Extending a timeout cannot fix a permanently covered button or a product defect that leaves it disabled.
4. Write Retryable Assertions That Describe State
A good assertion gives Cypress a condition worth retrying and gives a person useful failure evidence. Prefer an observable business state over a weak existence check.
describe('order status', () => {
it('shows the confirmed state after submission', () => {
cy.intercept('POST', '/api/orders').as('createOrder')
cy.visit('/checkout')
cy.get('[data-cy=place-order]').should('be.enabled').click()
cy.wait('@createOrder').its('response.statusCode').should('eq', 201)
cy.get('[data-cy=order-status]', { timeout: 10000 })
.should('be.visible')
.and('have.text', 'Confirmed')
})
})
The alias proves that the expected request completed with the intended protocol result. The UI assertion separately proves that the application rendered the customer-facing outcome. Neither a fixed delay nor a broad page-level selector is needed.
Match the assertion to the requirement. Use have.text when exact text is the contract and contain.text when surrounding content may legitimately vary. Use have.length for a completed list count and have.length.at.least when pagination or progressive loading makes a minimum the real requirement. Avoid assertions that pass too early, such as checking only that a container exists before its data has rendered.
Negative assertions deserve special care. should('not.exist') can pass immediately if a loading indicator has not appeared yet, even if it appears a moment later. Anchor the sequence in a positive event, such as waiting for the request or first confirming the indicator appears, then assert its disappearance when that sequence is part of the requirement.
The Cypress cy.intercept examples guide covers route matching and response assertions in more depth.
5. Use should Callbacks for Derived Conditions
The callback form of .should() is useful when one yielded subject must satisfy several related expectations or when the value needs synchronous transformation. Cypress invokes the callback again whenever an assertion inside throws, so the callback must be safe to repeat.
cy.get('[data-cy=invoice-summary]', { timeout: 10000 }).should(($summary) => {
const subtotal = Number($summary.find('[data-cy=subtotal]').text().replace('#39;, ''))
const tax = Number($summary.find('[data-cy=tax]').text().replace('#39;, ''))
const total = Number($summary.find('[data-cy=total]').text().replace('#39;, ''))
expect(subtotal, 'subtotal').to.be.greaterThan(0)
expect(tax, 'tax').to.be.at.least(0)
expect(total, 'calculated total').to.eq(subtotal + tax)
})
Each callback execution reads the current summary descendants and calculates values locally. It does not mutate the page, send a request, increment a counter, or write external state. If the total updates after the subtotal, Cypress can retry the whole get plus callback until the consistent state arrives.
Do not call Cypress commands inside a .should() callback. Current Cypress versions reject that pattern. Queue commands before or after the callback instead. Also avoid side effects in ordinary JavaScript: a callback may execute many times, so analytics calls, fixture mutation, and random data generation can create duplicate or shifting results.
The callback return value is ignored. Cypress continues yielding the previous subject. If you need to transform a resolved value once and use the transformed result in the chain, .then() can return it. If the transformation must be recalculated until an expectation succeeds, keep synchronous calculation and assertions inside .should().
Label expect calls as shown. A timeout with expected total to equal... is much easier to investigate than an unlabeled numeric mismatch.
6. Know When then Does Not Retry
.then() runs its callback once when the preceding command resolves. It is appropriate for one-time branching, extracting a resolved property, invoking additional Cypress commands in sequence, or passing a subject to non-retryable setup logic. It is not a drop-in retryable assertion container.
cy.get('[data-cy=user-card]').then(($card) => {
const userId = $card.attr('data-user-id')
expect(userId).to.match(/^usr_/)
cy.request(`/api/users/${userId}`).its('status').should('eq', 200)
})
The card query resolves according to its own existence behavior, then the callback runs once. If data-user-id is added after the element first appears, the expect inside .then() does not cause cy.get() to run again. Put that evolving attribute assertion in .should() first:
cy.get('[data-cy=user-card]')
.should('have.attr', 'data-user-id')
.and('match', /^usr_/)
Be aware that some Chai and Chai-jQuery chainers change the yielded subject. have.attr with no expected value yields the attribute string, which is why .and('match', ...) works above. Other assertions retain the element. Read the chainer contract when subsequent commands receive an unexpected type.
A common stale-element pattern captures $button in .then(), performs several actions through cy.wrap($button), and assumes the framework will not rerender. Prefer a fresh query for each state-changing action. If a one-time callback is truly needed, finish the immediate calculation there and return to selectors that can obtain current DOM state.
7. Set Timeouts at the Real Slow Boundary
Cypress uses defaultCommandTimeout for most DOM commands, with separate settings for page loads, requests, responses, tasks, and executable processes. A timeout answers one question: how long can this operation legitimately take before its absence is a failure?
Use a local override when one known boundary is slower:
cy.get('[data-cy=export-complete]', { timeout: 30000 })
.should('be.visible')
.and('contain.text', 'Download ready')
The timeout on get flows to the chained assertion. Cypress can pass much earlier. This is better than setting every command to 30 seconds, which delays feedback for selectors that are simply wrong.
A sensible TypeScript configuration keeps the global default modest and makes other intent explicit:
import { defineConfig } from 'cypress'
export default defineConfig({
defaultCommandTimeout: 6000,
requestTimeout: 5000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.ts',
},
})
These numbers are examples, not universal targets. Measure the application and CI environment, define service expectations, and set limits that reveal regressions. Do not copy a large timeout merely to make a red build green.
When a command times out, read which condition never became true. Check the selector, page state, prior request, and overlays. A timeout increase is justified only when the condition arrives correctly but sometimes later within an accepted performance boundary. If latency itself violates the requirement, the timeout is correctly exposing a product issue.
8. Synchronize Network and UI Boundaries
Retrying the DOM is often enough, but an aliased request makes a known asynchronous boundary explicit and improves diagnosis. Register the intercept before the action that triggers it.
it('refreshes the account balance', () => {
cy.intercept('GET', '/api/accounts/*/balance').as('getBalance')
cy.visit('/account')
cy.get('[data-cy=refresh-balance]').click()
cy.wait('@getBalance').then(({ request, response }) => {
expect(request.method).to.eq('GET')
expect(response?.statusCode).to.eq(200)
})
cy.get('[data-cy=balance]')
.should('be.visible')
.and('not.have.text', 'Loading')
})
The request wait says the application completed the expected server interaction. The following query says the DOM eventually reflected it. Avoid assuming that a response immediately means rendering is complete. Framework scheduling, state normalization, and animation can still occur after the network event, so the UI assertion remains retryable.
Use route matchers narrow enough to identify the operation. A broad **/api/** alias may be consumed by an unrelated background request. If the application polls, reason about which occurrence the test needs and avoid a wait that can accidentally match an earlier call.
Stubbing can make state deterministic, but it changes the test boundary. A fixture-backed intercept verifies UI handling of a controlled response, not integration with the real service. Keep separate contract or integration coverage where needed. The retry strategy should match the purpose of the test rather than hiding an unstable shared environment.
Never use cy.wait(5000) as a substitute for identifying the request or visible state. If the operation finishes in 200 milliseconds, the suite wastes time. If it takes 5.1 seconds, the test still fails.
9. Keep Actions at Clear Chain Boundaries
An action command waits until its subject is actionable and then executes once. After the action, the application may replace the element or its ancestors. Chaining more operations from the old subject can therefore be fragile even when the syntax is legal.
Prefer this:
cy.get('[data-cy=country]').select('Canada')
cy.get('[data-cy=province]').should('be.enabled').select('Ontario')
cy.get('[data-cy=tax-rate]').should('have.text', '13%')
Over a long subject chain:
cy.get('[data-cy=country]')
.select('Canada')
.parent()
.find('[data-cy=province]')
.select('Ontario')
The first version reacquires the province after the country action and lets Cypress retry against the current DOM. It also shows the workflow as three observable phases. The second version assumes that the original ancestry remains valid through a rerender.
Do not place .click(), cy.request(), data creation, or logging with external effects inside .should(). If the assertion retries, the effect repeats. This is especially dangerous for create, charge, submit, and delete behavior. A retryable callback must be idempotent and normally read-only.
Custom abstractions need the same discipline. A custom command may contain built-in queries that retry, but the command itself is not automatically replayed as one atomic unit. For synchronous DOM lookup that genuinely needs query semantics, Cypress supports custom queries. Read Cypress custom commands and queries before adding a global abstraction.
10. Separate Retry-ability from Test Retries
Cypress retry-ability and Cypress test retries solve different problems. Command retry-ability waits within one test attempt for an expected condition. Test retries start another attempt after the attempt has already failed.
| Dimension | Command retry-ability | Test retries |
|---|---|---|
| Scope | Linked query and assertion | Entire test attempt |
| Trigger | Assertion has not passed yet | Test attempt failed |
| State | Same attempt and browser flow | New attempt with normal test setup |
| Main purpose | Synchronize with eventual application state | Surface flake or recover from transient attempt failure |
| Configuration | Command timeout and query structure | retries configuration |
| Misuse | Fixed waits or side effects in callbacks | Masking deterministic defects |
A conservative run-mode configuration looks like this:
import { defineConfig } from 'cypress'
export default defineConfig({
retries: {
runMode: 2,
openMode: 0,
},
e2e: {
baseUrl: 'http://localhost:3000',
},
})
This does not cause .click() to retry inside one attempt. It allows up to two new attempts in cypress run. Cypress also has experimental retry strategies in current releases, but they have different configuration constraints and should be adopted only after the team understands their reporting semantics. Stable retry-ability patterns do not depend on those experiments.
Treat a pass on retry as a quality signal, not proof that the test is healthy. Record the failing attempt, identify whether the cause is application state, test isolation, infrastructure, or a race, and assign ownership. Unlimited or high retries can turn a meaningful intermittent defect into a slow green pipeline.
11. Debug a Cypress should Retry Failure
Start with the final assertion message and work backward through the command log. The most useful question is not simply why the test timed out, but which expected state never became observable.
Use this investigation sequence:
- Confirm the test began with the intended URL, identity, tenant, feature flags, and seeded data.
- Inspect the selector at failure time. Determine whether it matches zero, one, or multiple current elements.
- Check the action immediately before the retrying chain. Confirm it fired once and targeted the intended control.
- Inspect aliased request details, response status, and relevant payload shape.
- Look for an overlay, animation, disabled state, or rerender that changes actionability.
- Decide whether the assertion represents the actual business requirement.
- Compare local and CI browser, viewport, data, and environment configuration.
Add temporary evidence that preserves behavior. A narrow cy.intercept() alias, a screenshot on the failing state, and server request identifiers are generally more useful than a sleep. Avoid adding commands inside a .should() callback for diagnostics, because current Cypress disallows it and because the callback can run repeatedly.
If the app exposes a clear loading state, assert its meaningful completion. If no reliable signal exists, that is design feedback: testability often improves when the UI exposes stable roles, status text, or data-cy contracts and the backend returns traceable request IDs.
Reproduce with the same browser and command used in CI. Interactive mode can differ in speed and retained command snapshots, so a local pass alone does not invalidate CI evidence.
12. Cypress Retry-ability Best Practices
Build retry behavior into test design rather than adding it after failures:
- Begin with deterministic data. Seed through a supported API or task and isolate records by test.
- Use stable selector contracts. Prefer accessible user-facing selectors or dedicated test attributes over layout classes.
- Assert meaningful state. A visible confirmation is better evidence than a container merely existing.
- Register intercepts before triggering requests. Give aliases operation-specific names.
- Put one state-changing action at a clear boundary, then query the resulting state again.
- Keep
.should()callbacks synchronous, read-only, and safe to repeat. - Apply local timeouts to legitimately slow operations and measure why they need extra time.
- Keep test retries low and report passes on later attempts as flake signals.
- Make failures diagnosable with request IDs, screenshots, logs, and retained CI artifacts.
- Review negative assertions for early-pass risk. Anchor them to a positive event when sequence matters.
Component tests can make retry failures easier to isolate because the rendered surface and data are smaller. The Cypress component testing example shows how to control component state without a full backend. End-to-end tests should remain focused on critical integration behavior.
A healthy suite rarely needs a helper named waitUntilEverythingIsReady. It expresses readiness as specific evidence at each boundary: the request completed, the button became enabled, the row appeared, or the status changed. Those conditions document the product and give Cypress a precise retry target.
Interview Questions and Answers
Q: What exactly does Cypress retry?
Cypress retries linked queries and assertions from the top of their query chain until the assertions pass or the timeout expires. Non-query commands generally execute once. Action commands repeatedly check actionability before firing the action once.
Q: Why is cy.wait(2000) weaker than .should()?
A fixed wait always consumes two seconds and makes no statement about required state. It can be longer than necessary on fast runs and too short on slow runs. A retryable assertion continues as soon as a specific observable condition becomes true.
Q: What is the difference between .then() and .should(callback)?
A .then() callback runs once after the previous command resolves. A .should() callback can run repeatedly until all assertions stop throwing. Therefore, a should callback must contain no commands or non-idempotent side effects.
Q: Does Cypress retry a failed click?
Cypress retries the queries and actionability checks that lead to a click. Once the element is actionable, the actual click executes once. A later failed assertion does not cause that click to fire again within the same chain.
Q: How would you choose a command timeout?
I start from the accepted latency of the operation and evidence from CI. I keep a reasonable suite default and override the query at a known slow boundary. I do not raise timeouts to hide wrong selectors, permanent overlays, or performance regressions.
Q: How are test retries different from retry-ability?
Retry-ability operates inside one attempt while state is still evolving. Test retries rerun the entire test after an attempt fails. Test retries can expose flake, but they do not repair poor synchronization inside a test.
Q: How do you avoid stale subjects after a rerender?
I end the chain at a state-changing action and start a new query for the next state. That lets Cypress locate the current DOM rather than carrying a reference through a framework update. Stable selectors and short chains make the boundary clear.
Q: What makes a retryable assertion diagnostically strong?
It names a business-relevant state, uses a precise selector, and fails with useful expected and actual values. When a network boundary matters, I pair it with a narrowly matched alias. I also retain screenshots and request identifiers in CI.
Common Mistakes
- Assuming every Cypress command is retried. Queries and assertions retry, while side-effecting commands have different rules.
- Putting
click,request, data creation, or mutable counters inside a.should()callback. The callback may run many times. - Using
.then()for a value that appears after the element itself. The callback assertion runs only once. - Chaining through an action when the framework replaces the subject or its ancestors. Query again after the action.
- Raising
defaultCommandTimeoutacross the suite to hide one slow or broken boundary. - Registering an intercept after the application already sent the request.
- Matching a broad network route that can be consumed by unrelated traffic.
- Relying on
should('not.exist')before proving that the relevant workflow started. - Enabling several whole-test retries and ignoring tests that pass only on later attempts.
- Adding fixed waits during debugging and leaving them in the committed test.
Conclusion
Cypress retry-ability works best when a test is written as a sequence of observable states. Link queries to assertions, keep actions at explicit boundaries, reacquire DOM state after rerenders, and use network aliases where a request is part of the contract. Cypress will then wait only as long as the application actually needs.
Start by replacing one fixed wait with a precise UI or network condition. If that condition still times out, investigate the product state and test setup rather than immediately extending the clock. That practice produces faster tests, clearer failures, and a suite that reveals flakiness instead of concealing it.
Interview Questions and Answers
Explain Cypress retry-ability in one minute.
Cypress classifies chained operations by behavior. Queries link and rerun from the top when an assertion has not passed, while non-query commands normally execute once. This lets tests wait on expected state without fixed sleeps.
What is retried when get, find, and should are chained?
If the assertion fails, Cypress repeats `get`, `find`, and the assertion as one linked query chain. This is important when a framework replaces DOM nodes during rendering. The assertion eventually evaluates the current matching elements.
Why must a should callback be idempotent?
Cypress can invoke the callback many times before it passes. Any mutation, request, or business action could therefore occur more than once. I keep the callback synchronous and read-only, with only deterministic calculations and assertions.
How do action commands participate in retry behavior?
Cypress retries their prerequisite queries and built-in actionability checks. When the target is actionable, it performs the action once. I then start a new query chain to observe the resulting state.
How would you fix a test that waits five seconds before checking a toast?
I would identify the event that produces the toast, alias its request if relevant, perform the action, and assert the toast's visible text. The assertion can retry and pass immediately when the toast appears. I would remove the fixed delay.
When is a local timeout override justified?
It is justified for a measured operation whose accepted latency is longer than the normal command limit, such as a report export. I put the timeout on the query at that boundary and keep other failures fast. A timeout is not a repair for incorrect state.
How do you distinguish application flake from test flake?
I inspect initial state, action evidence, requests, DOM transitions, and repeat-attempt artifacts. If the application sometimes produces the wrong state under the same controlled inputs, that is product behavior. If the test races, shares data, or observes the wrong signal, the test design owns the flake.
What is your rule for whole-test retries?
I keep them low, use them to preserve evidence and classify intermittent failures, and report later-attempt passes. A retry does not close the defect. The team still investigates and fixes the unstable product, environment, or test boundary.
Frequently Asked Questions
What is Cypress retry-ability?
Cypress retry-ability reruns linked queries and assertions while an expected condition is not yet true. It stops as soon as the assertion passes or fails when the applicable timeout expires.
Does Cypress retry cy.get automatically?
Yes. `cy.get()` is a query and can be rerun with its linked queries when a downstream assertion has not passed. The chain obtains current DOM state rather than relying on one permanently captured element set.
Does Cypress retry click commands?
Cypress retries the query and actionability checks leading to `.click()`, then performs the click once. It does not repeat the click because a later assertion fails.
How long does Cypress retry an assertion?
Most DOM assertions use the timeout inherited from the preceding query, commonly `defaultCommandTimeout` unless overridden. The timeout is a maximum, so Cypress continues immediately when the condition passes.
Why does an assertion in cy.then not retry?
A `.then()` callback is designed to run once after the previous command resolves. Use `.should(callback)` for synchronous, repeatable assertions against evolving state, and keep all side effects outside that callback.
Should I increase defaultCommandTimeout for flaky Cypress tests?
Only when evidence shows a valid operation can exceed the existing limit. Prefer a local timeout at the measured slow boundary, and first rule out wrong selectors, bad data, overlays, and missed requests.
Are Cypress test retries the same as retry-ability?
No. Retry-ability reevaluates queries and assertions inside one attempt, while test retries start a new attempt after failure. Whole-test retries should remain a visible flake signal.