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.