Cypress: Timed Out Retrying After 4000ms, and What It Really Means

10 min readupdated August 26, 2026

The test stops four seconds in:

CypressError: Timed out retrying after 4000ms:
Expected to find element: [data-testid="toast"], but never found it.

The first instinct is to raise the timeout. That is almost always wrong, and it usually turns a four second failure into a ten second failure with the same cause underneath.

The error is more useful than it looks. It tells you how long Cypress waited, and that number tells you which timeout fired.

Cypress has four timeouts, not one

Most of the confusion here comes from treating "timeout" as a single setting. It is four, with different defaults.

Setting Default What it covers
defaultCommandTimeout 4 seconds Most DOM commands, like finding an element
requestTimeout 5 seconds Waiting for a request to be sent in cy.wait()
responseTimeout 30 seconds Waiting for a response to come back
pageLoadTimeout 60 seconds cy.visit() and page transitions

So the number in your error already tells you where to look. 4000ms is a DOM command. 60000ms is a page that never finished loading, which is a completely different problem with a completely different fix.

You can change each one:

// cypress.config.js
module.exports = defineConfig({
  defaultCommandTimeout: 4000,
  requestTimeout: 5000,
  responseTimeout: 30000,
  pageLoadTimeout: 60000,
});

What "retrying" actually means

This word matters, because it rules out a whole class of guesses.

Cypress did not look once and give up. It kept re-running the query for the full four seconds. So the element was not there at any point in those four seconds. That is a stable condition, not a near miss.

This is why "it just needed a bit longer" is usually wrong. If four seconds of continuous retrying found nothing, the thing you are waiting for is not late. It is not coming.

The five real causes

1. The selector does not match

Ordinary, and worth ruling out first because it costs nothing. Open the app and check in the browser console:

document.querySelectorAll('[data-testid="toast"]').length

If that returns 0 with the page in the right state, no timeout will ever help you.

2. The thing genuinely never happens

The save failed, the request 500'd, a validation error blocked submission. Your test is waiting for a success message that the application never produced. The test is correct and the application is broken. This is the case people miss, because they assume the test is at fault.

Look at the Cypress command log and the network panel before changing any config.

3. You are asserting on the wrong state

Waiting for a toast that already appeared and disappeared. Toasts often auto-dismiss in three seconds, so a slow earlier step can mean you start looking after it has gone.

4. The element exists but not where you are looking

Inside an iframe, which Cypress cannot see into without extra work, or rendered in a portal outside the container you scoped to. We cover the frame case in handling iframes.

5. The page never finished loading

If the number is 60000, this is your answer. cy.visit() waits for the load event. A request that never resolves will hold it open for the full minute.

Do not fix it with cy.wait(number)

cy.wait(5000); // please do not
cy.get('[data-testid="toast"]').should('be.visible');

A fixed wait is wrong in both directions. It costs five seconds on every run when the toast appeared in 200ms, and it still fails on the day the server is slow. Multiply that across a suite and you have a run that is both slower and no more reliable.

The right tool is an assertion, because assertions retry:

cy.get('[data-testid="toast"]').should('be.visible');

That continues the moment the toast appears, and only fails if it genuinely never does. Faster and more reliable at the same time.

When a fixed wait is acceptable: waiting on a specific network call, using an alias. That is not a guess, it is waiting for a real event:

cy.intercept('POST', '/api/contacts').as('save');
cy.get('[data-testid="save"]').click();
cy.wait('@save');
cy.get('[data-testid="toast"]').should('be.visible');

When raising the timeout is correct

Sometimes the work really is slow. A report that takes eight seconds to generate is not a bug.

Raise it for that one command rather than globally:

cy.get('[data-testid="report"]', { timeout: 15000 }).should('be.visible');

Raising the global default hides genuine regressions everywhere else. A page that used to respond in one second and now takes nine is a real problem, and a fifteen second global timeout means nobody notices until a customer does.

Raise a timeout when the work is genuinely slower. Fix the cause when the thing never happens.

Read the command log first

This is Cypress's real advantage and people skip it.

Hover the failed command in the log and Cypress shows you the page at that moment in the test. You can see whether the toast was there, whether the form was still open, whether an error banner appeared instead. That answers the question in seconds, without changing a line of config.

Remember this

The number tells you which of the four timeouts fired, so read it before anything else. Retrying means the element was absent for the whole period, not that it was late. Check the selector, then check whether the application actually did the thing. Use retrying assertions rather than fixed waits, alias a network call when you must wait on one, and raise a timeout only for the single command that is genuinely slow.

The Cypress track goes deeper on the retrying that all of this rests on: how retrying really works.

Related: element is being covered by another element, element detached from the DOM, and how to fix flaky tests.

Reference: Cypress configuration.

now prove it

The save test is flaky

Reading is the easy part. Write the test yourself, then we break the app on purpose to check it would really catch the bug.

Solve this problem →

Keep reading