QA How-To
How to Fix Cypress "Timed out retrying after 4000ms"
Fix Cypress timed out retrying after 4000ms errors by checking selectors, page state, network waits, assertions, and CI readiness with verified examples.
18 min read | 3,568 words
TL;DR
Check whether the failing Cypress command is looking for the wrong element, running on the wrong page, or racing a request or render. Fix that cause, then use a local timeout only if healthy work truly exceeds four seconds.
Key Takeaways
- Read the failed command and full error text before changing a timeout.
- A 4000ms DOM retry is different from page-load and aliased-request timeouts.
- Register intercepts before the request and assert both response and rendered UI.
- Use retryable .should() assertions for values that change after rendering.
- Wait for overlays to clear instead of forcing clicks through blocked UI.
- Scope longer timeouts to measured slow operations and reproduce CI failures in CI mode.
To fix Cypress Timed out retrying after 4000ms errors, first identify the command printed above the error. It appears when a retryable DOM query or assertion cannot become true within its timeout, often because the selector is wrong, the page is in the wrong state, or the application has not finished rendering. Read the failed command and the DOM snapshot before increasing a timeout.
Timed out retrying after 4000ms: Expected to find element: '[data-cy="save-order"]', but never found it.
The example message means Cypress kept looking for that selector and never found a matching element. Your failure may instead say an element was not visible, was covered, or that an assertion expected a different value. Those details change the fix. This guide uses an orders page as a concrete example: the app has /orders, a GET /api/orders request, a button marked data-cy="refresh-orders", and rows marked data-cy="order-row". Substitute your application's actual route and selectors. If you are setting up a suite from scratch, the Cypress framework guide covers the surrounding project structure.
TL;DR
Open the failure in Cypress and read the exact command and subject. Inspect the page at that point, confirm the selector against the rendered DOM, and wait for the event that makes the expected state possible. For a page that renders orders after an API call, register the intercept before the visit, wait for the response, then assert on the rows:
// cypress/e2e/orders-loading.cy.ts
describe('orders loading', () => {
it('shows an order after the response arrives', () => {
cy.intercept('GET', '/api/orders').as('getOrders')
cy.visit('/orders')
cy.wait('@getOrders').its('response.statusCode').should('eq', 200)
cy.get('[data-cy="order-row"]').should('have.length.at.least', 1)
})
})
Run npx cypress run --spec cypress/e2e/orders-loading.cy.ts after starting your app and setting e2e.baseUrl to its address. A passing spec confirms this particular loading path; it does not prove that every four-second timeout in the suite has the same cause. For more detail on the interception sequence, see waiting for an API response in Cypress.
What the Error Actually Means
Cypress's defaultCommandTimeout defaults to 4000 milliseconds. A query such as cy.get() retries until it finds an element; an attached .should() assertion also retries its query chain until the assertion passes or that budget expires. The wording reports the elapsed retry budget, not a universal limit for an entire test. cy.click() has actionability checks that wait for an element to become actionable. By contrast, cy.visit() uses pageLoadTimeout, and cy.wait('@alias') has distinct requestTimeout and responseTimeout periods. Changing the wrong setting leaves the actual failure untouched.
Read the first line of the failed command and the rest of the message together. "Expected to find element" directs you to selector or state. "Expected ... to be visible" says the element exists but its presentation is wrong. An expected text mismatch points to data, formatting, or a stale assertion. If an alias never sees a request, check when the intercept was registered and whether the browser actually sent that request. Cypress retries queries and assertions, but arbitrary JavaScript in .then() is not rerun. The Cypress retry-ability guide explains this boundary in more detail.
A larger number can be appropriate for a genuinely slow operation, but it is evidence only if the expected state eventually arrives. Inspect the command log's snapshot and your browser's network panel. If a 10-second run still points at the wrong element, a 30-second run merely delays the same failure.
Root-Cause Decision Table
| Symptom at failure | Likely root cause | Targeted fix |
|---|---|---|
| "Expected to find element" and the DOM has a differently named control | Selector drift | Inspect the current markup and use a stable data-cy attribute or a precise text query. |
| The selector is correct but the page shows login, an empty state, or another route | Missing prerequisite or wrong navigation | Establish the required state and assert the route or page identity before querying. |
| A loader remains and the expected row arrives after an API response | Network-driven rendering | Register cy.intercept() before the triggering action, wait on its alias, then assert the UI. |
A value changes after a .then() callback already checked it |
Non-retrying assertion | Put the condition in .should() so Cypress can re-query. |
| The element exists but click says it is covered or not visible | Overlay, animation, or disabled state | Wait for the real blocking state to clear and assert actionability. |
| The operation consistently takes more than four seconds in healthy runs | Legitimate slow work | Give that query a measured, local timeout and investigate the app's latency. |
| Only CI or Docker fails, with blank or incomplete screenshots | Server readiness or container networking | Wait for the app URL from the same execution environment and use the correct host. |
Use the table as a first hypothesis, then reproduce with one spec. Do not treat the text "after 4000ms" as a diagnosis by itself. The following fixes each include a verification command; keep the one that matches your observed failure.
1. Fix Cypress Timed out retrying after 4000ms When the Selector Is Wrong
Start with the DOM captured at the failing command. Cypress's command log snapshot shows what existed at that moment, while DevTools lets you inspect the live node and attributes. A class name generated by a CSS module or component library can change between builds; data-cy is an explicit contract that test and UI owners can maintain. If the target is a save button, mark that button in your application as data-cy="save-order" and test the same attribute. Do not guess a selector from an earlier screenshot.
// cypress/e2e/order-selector.cy.ts
describe('order form selector', () => {
it('finds the actual save control', () => {
cy.visit('/orders/new')
cy.get('[data-cy="order-form"]').should('be.visible')
cy.get('[data-cy="save-order"]').should('exist').and('be.enabled')
})
})
Verify with npx cypress run --spec cypress/e2e/order-selector.cy.ts. If it still fails, inspect the form container before changing the timeout: perhaps the save control is inside a dialog that has not opened, or your app uses another test ID. should('exist') proves a DOM match, while should('be.visible') checks presentation; use the assertion that matches the behavior you care about. For dynamic lists, a selector on the row should be more stable than :nth-child(3), because filtering and sorting can move items.
A scoped query can avoid matching an unrelated save button elsewhere on the page. For example, cy.get('[data-cy="order-form"]').find('[data-cy="save-order"]') re-queries inside the intended container. If you need guidance on choosing durable attributes, see Cypress data-cy selectors. Keep accessible names useful too: cy.contains('button', 'Save order') is a good option when that wording is part of the user interface contract.
2. Fix Cypress Timed out retrying after 4000ms When the Page State Is Wrong
A perfect selector cannot find an order row on a login page. Look at cy.url(), the visible heading, and the response to the navigation request. A redirect to /login can be caused by expired session data, a missing seeded account, or a CI environment pointed at the wrong deployment. An empty-state screen can mean the user genuinely has no orders. Both situations deserve explicit setup, not a longer wait.
// cypress/e2e/order-state.cy.ts
describe('orders page state', () => {
it('loads the intended page and known data', () => {
cy.intercept('GET', '/api/orders', {
statusCode: 200,
body: [{ id: 'order-101', number: 'QA-101' }],
}).as('getOrders')
cy.visit('/orders')
cy.location('pathname').should('eq', '/orders')
cy.wait('@getOrders')
cy.get('[data-cy="orders-page"]').should('be.visible')
cy.get('[data-cy="order-row"]').should('contain.text', 'QA-101')
})
})
Run npx cypress run --spec cypress/e2e/order-state.cy.ts. This code assumes the sample app renders number from /api/orders; change the response shape to your actual API contract. If the URL assertion fails, resolve authentication or routing before investigating rows. If the URL passes but the page marker fails, check whether the route has an error boundary or a different loading screen. A stub is appropriate for a UI-state test; separately keep an end-to-end test that exercises your real backend. Stubbed data cannot prove that the server returns orders in production.
For authenticated pages, establish the session using your app's supported login flow or setup endpoint. Avoid relying on a previous test to leave a logged-in browser behind. Cypress test isolation can clear browser context between tests, and independently runnable tests make the failure easier to reproduce. If the issue appears only after navigation between routes, assert the route transition first, then query elements on the destination page.
3. Synchronize the Request That Supplies the UI
A row may be absent because the page has not received its orders yet. Register an intercept before cy.visit() when loading begins during page initialization; registering it after the visit can miss the request. If a click starts the fetch, register the alias before the click. cy.wait('@getOrders') gives the request and response cycle a separate timeout budget, then the DOM assertion checks whether the app rendered the data. This exposes a server error instead of burying it under a missing-element timeout.
// cypress/e2e/order-network.cy.ts
describe('orders request and render', () => {
it('renders rows after refresh', () => {
cy.visit('/orders')
cy.intercept('GET', '/api/orders').as('getOrders')
cy.get('[data-cy="refresh-orders"]').click()
cy.wait('@getOrders').its('response.statusCode').should('eq', 200)
cy.get('[data-cy="loading"]').should('not.exist')
cy.get('[data-cy="order-row"]').should('have.length.at.least', 1)
})
})
Verify with npx cypress run --spec cypress/e2e/order-network.cy.ts. If the alias times out, inspect the method and URL, including query parameters. If it resolves with a non-200 response, fix the API or the test data. If it resolves successfully but the row is missing, the problem is in rendering or in the response shape the component expects. Keep the network and UI assertions separate so the first failed assertion identifies the boundary.
An intercept sees network traffic, not a response served entirely from browser cache. The app may also render from memory without another request. In those cases, wait for a user-visible state such as the loading indicator disappearing or the row appearing, and investigate caching only if the application contract requires a fetch. Avoid cy.wait(4000): it can finish too early on slow machines and wastes time on fast ones. The cy.intercept guide and alias waiting examples show related patterns.
4. Keep Changing Values Inside Retryable Assertions
Cypress does not rerun a .then() callback to see whether an app value eventually changes. That callback is useful for inspecting or transforming a resolved value, but a one-time expect() inside it can be premature. A .should() assertion attached to a query gives Cypress a condition it can retry. This distinction matters for counters, validation messages, save status, and data that arrives after a render cycle.
// cypress/e2e/order-status.cy.ts
describe('save status', () => {
it('waits for the status to change', () => {
cy.visit('/orders/new')
cy.get('[data-cy="order-name"]').type('QA sample')
cy.get('[data-cy="save-order"]').click()
cy.get('[data-cy="save-status"]').should('have.text', 'Saved')
})
})
Run npx cypress run --spec cypress/e2e/order-status.cy.ts. The app must expose the named fields and status; adapt the selectors and expected text to the actual UI. If the status stays at "Saving", inspect the save request, validation errors, and app logs. If it changes to "Failed", a retrying assertion will correctly fail after its budget; there is no reason to hide that signal.
Do not store a DOM node in a variable and assert against that same node after a framework re-renders it. Query the selector again so Cypress observes the replacement element. A chained cy.get(...).should(...) can re-run its query; a native document.querySelector() value captured in .then() cannot be re-fetched by Cypress. Also distinguish test retries from command retry-ability: configuring retries reruns the whole failed test, while .should() retries the assertion chain within the current attempt. The former can expose flakiness but cannot make a wrong assertion correct.
5. Resolve Actionability Failures Instead of Forcing Clicks
Sometimes Cypress finds the button but will not click it. The failure may report that the target is covered by another element, disabled, detached, or not visible. A modal backdrop, an animation, a sticky header, or a loading overlay can explain that state. Inspect the screenshot and the element Cypress says covers the target. Use a condition tied to the blocker, then click the intended control.
// cypress/e2e/order-actionability.cy.ts
describe('save button actionability', () => {
it('clicks only after loading ends', () => {
cy.visit('/orders/new')
cy.get('[data-cy="loading-overlay"]').should('not.exist')
cy.get('[data-cy="order-name"]').type('QA sample')
cy.get('[data-cy="save-order"]').should('be.visible').and('be.enabled').click()
cy.get('[data-cy="save-status"]').should('have.text', 'Saved')
})
})
Run npx cypress run --spec cypress/e2e/order-actionability.cy.ts. If the overlay is hidden rather than removed, use should('not.be.visible') instead of should('not.exist'). If the button remains disabled, inspect form validation or a missing prerequisite; a disabled state can be correct product behavior. Cypress's normal click checks scroll position, visibility, covering elements, and other actionability conditions. click({ force: true }) skips those checks and can make a test pass while a user still cannot complete the action. Reserve forced interactions for cases where bypassing user behavior is the explicit purpose of the test, and document why.
A detached-element failure usually means the framework replaced a node between locating and acting on it. Re-query after the operation that causes the replacement. For example, after changing a filter, locate the new row by its stable ID instead of holding on to a previous row element. When an animation is the source, prefer an app-level readiness signal over a guessed sleep. A wait for aria-busy="false", a disappeared spinner, or an enabled button states the behavior the test actually needs.
6. Increase One Timeout Only When Healthy Work Really Takes Longer
A legitimate task can exceed four seconds: a report generation step, a large client-side import, or a known slow search in a test environment. Measure its normal duration and choose a budget that leaves room for expected variation. Scope that budget to the query that observes completion. A global defaultCommandTimeout change applies to many commands and makes wrong selectors take longer to fail, so start local.
// cypress/e2e/order-export.cy.ts
describe('order export', () => {
it('reports completion within its measured budget', () => {
cy.visit('/orders')
cy.get('[data-cy="export-orders"]').click()
cy.get('[data-cy="export-status"]', { timeout: 12000 })
.should('have.text', 'Export ready')
})
})
Verify with npx cypress run --spec cypress/e2e/order-export.cy.ts. The 12-second value is illustrative, not a recommended universal setting. If your healthy exports usually finish much sooner, reduce it; if they routinely approach the limit, inspect the service and agree on a performance expectation. The command waits up to the limit and proceeds as soon as the assertion passes. It does not always sleep for 12 seconds.
If a suite genuinely needs a wider default because of its environment, set defaultCommandTimeout in cypress.config.ts or pass --config defaultCommandTimeout=10000 to cypress run. Before doing so, classify a few failures and confirm that they actually finish under the new budget. pageLoadTimeout affects page load, not the cy.get() in the example. For an alias, tune requestTimeout or responseTimeout on cy.wait() after finding which phase is slow. Cypress documents their defaults separately, so do not infer their values from a 4000 ms DOM error.
7. Fix CI and Docker Environment Delays at Their Source
If the same spec passes locally but fails in CI, capture the failed screenshot and check whether the app loaded at all. In cypress run, failure screenshots are saved under cypress/screenshots by default. A page stuck on a connection error, an empty shell, or a login redirect points to setup, host resolution, or missing environment data. Run one spec in the same mode and environment as CI before changing assertions. See capturing Cypress failure screenshots for the artifact workflow.
For a CI job where the app and Cypress run on the same machine, start the server and wait for its health URL before the browser test. wait-on is a real npm utility; install it as a dev dependency if your project does not already have it. Replace the URL with your app's readiness endpoint and keep the server process alive during Cypress:
npm install --save-dev wait-on
npm run dev -- --host 127.0.0.1 > /tmp/orders-app.log 2>&1 &
npx wait-on http://127.0.0.1:8080/orders
npx cypress run --spec cypress/e2e/orders-loading.cy.ts --config baseUrl=http://127.0.0.1:8080
Verify with the final command's exit code and the screenshot directory on failure. A listening port alone is weaker than a page or health endpoint that confirms the app is ready. If your app uses another port, change both occurrences. If /orders requires authentication and redirects, use an unauthenticated health endpoint for readiness and test login separately.
Inside Docker, localhost means the container itself. When the app is a separate Compose service named web, point Cypress at http://web:8080; when the app is on the host, use the host address your Docker setup exposes. Match the cypress/included image tag to the Cypress version installed in your project; do not copy an arbitrary tag. For example, after checking your installed Cypress version, replace the image-tag placeholder and run:
docker run --rm --network your-compose-network \
-v "$PWD:/e2e" -w /e2e --entrypoint cypress \
cypress/included:<your-cypress-version> run \
--spec cypress/e2e/orders-loading.cy.ts \
--config baseUrl=http://web:8080
The displayed tag is a placeholder to replace before execution. Check the actual image tag and command behavior for your chosen image, then run the same spec and confirm that web resolves from inside the container. A Docker container cannot reach a sibling service through 127.0.0.1. If the app loads but only data requests fail, inspect API base URLs and credentials available to that container. The Cypress environment variables guide helps separate test configuration from application configuration.
How to Verify the Fix
Run the smallest failing spec first. Use npx cypress run --spec cypress/e2e/orders-loading.cy.ts for the running example, or substitute the exact spec path from your failure. Then run it repeatedly in the mode where it failed; a single pass may only mean the race did not occur this time. Keep screenshots and command logs for any failure, and compare the request timing, URL, and DOM state between attempts. A good fix changes the observable cause: the correct selector matches, the prerequisite exists, the alias sees the intended request, or the UI completes within a measured budget.
Next, run the surrounding suite. A focused intercept may accidentally match another request, and a stubbed response can hide a backend regression if every test uses it. Pair a deterministic UI test with at least one real-backend path. If you increased a timeout, record the normal completion range and ensure the test still fails when the expected state never arrives. If you changed the UI to add data-cy, confirm the attribute appears in the built page used by CI, not only in a local development branch.
For a suspected flake, note the number of attempts and failures rather than saying it "looks stable." Cypress test retries can help collect evidence, but a pass on retry still indicates a race or intermittent dependency worth investigating. The Cypress flaky-test guide covers a broader triage process. Review the final failure message: if it moved from a missing row to a 500 response, you have uncovered the next real fault rather than failed to make progress.
Prevent It From Coming Back
Give important controls stable selectors and make the app expose meaningful readiness states. Prefer a loading indicator that ends when data is committed to the UI, a disabled button while validation is incomplete, and a status message for save completion. These are useful to users as well as tests. Keep test data explicit: seed it through a supported setup route, fixture, or API, and name the specific record you expect. Avoid dependencies on test order and on whatever happens to be in a shared database.
Put intercepts immediately before the operation they observe. Assert the response where its status or payload matters, then assert the visible outcome. Use local command timeouts only for operations with known latency, and review unusually long budgets in code review. Keep CI server readiness checks, browser test commands, and screenshots in one documented pipeline so a failure can be reproduced. Do not silence command errors by adding blanket test retries; a retry is a diagnostic signal and a limited safety net, not a substitute for state control.
When a component frequently replaces nodes, re-query after each state transition. When a selector changes, update the test and the UI contract together. A concise failure report should name the failed command, current URL, relevant request status, and whether the expected element existed in the DOM. That information is far more useful than the phrase "Cypress timed out."
Interview Questions and Answers
Q: Why does Cypress show 4000ms here? The failing DOM command used the default command timeout. That number does not say how long the entire test ran.
Q: Does cy.wait('@orders') use that same timeout? No. Alias waits have a request phase and a response phase with their own configuration. Read the alias error to see which phase failed.
Q: Why can a correct selector still time out? The browser may be on the wrong route, the user may have no data, or the component may not have rendered yet. Inspect state before editing the selector.
Q: When should you use a fixed cy.wait(4000)? Almost never for synchronization. Wait for the request or observable UI condition instead; a clock duration does not prove completion.
Q: What is the difference between .then() and .should() for changing UI? .then() runs its callback once after the preceding command resolves. .should() can retry a query and assertion until they pass or time out.
Q: Can click({ force: true }) fix a covered-element error? It bypasses actionability checks but may hide a real overlay that blocks users. Remove or wait for the blocker when the test is meant to model user behavior.
Common Mistakes
- Raising
defaultCommandTimeoutfor every test before inspecting the failed selector makes unrelated failures slower and leaves the underlying cause in place. - Registering
cy.intercept()after a page-load request has already fired creates an alias that never sees that request. Define it beforecy.visit()when initialization makes the call. - Using
cy.wait(4000)as a proxy for an API response assumes the same timing on a laptop and a loaded CI worker. Observe the request or UI state. - Asserting against a DOM node saved before a re-render can check a detached element. Query the new node by a stable selector.
- Treating a successful intercepted response as proof of rendered content skips the UI boundary. Assert the row or message after the response.
- Enabling test retries without recording retry passes can make a flaky suite appear green. Track attempts and fix the race.
- Pointing a container at
localhostfor an app running in another container reaches the wrong network namespace. Use the service hostname on the shared network.
Conclusion
To fix Cypress "Timed out retrying after 4000ms", follow the failed command to the state Cypress could not observe. Correct a selector or prerequisite, align the test with the request and render sequence, and use a scoped timeout only when the work is demonstrably slow. Re-run the failing spec in its original environment and preserve the evidence from any remaining failure. The next actionable step is to compare your error's exact wording with the decision table, then apply and verify the matching fix.
Interview Questions and Answers
How would you triage a Cypress "Timed out retrying after 4000ms" failure?
I would inspect the exact failed command, assertion, URL, DOM snapshot, and relevant network requests. I would classify it as selector drift, wrong state, render timing, actionability, or genuinely slow work. Then I would change one cause and rerun the smallest failing spec in the same environment.
What does defaultCommandTimeout cover in Cypress?
It is the default retry budget for many commands and assertions, including DOM queries such as cy.get. It is not the total test duration. Page loads and aliased network waits have different timeout settings.
How do you avoid missing an initial API request with cy.intercept?
I register the intercept before cy.visit when the page makes the request during initialization. For a request triggered by a button, I register it before the click. I then wait on the alias and assert the rendered result.
When is a local timeout increase justified?
It is justified after measuring a healthy operation that regularly exceeds the default and confirming the selector and app state are correct. I put the timeout on the relevant query, not every command. I also investigate why the operation takes that long.
Why can an assertion inside .then() be flaky for live UI state?
The callback runs once against the value it receives. If the UI changes afterward, that assertion does not rerun. A query followed by .should() gives Cypress a retryable condition for changing state.
How would you handle a button covered by an overlay?
I would identify the covering element from the failure and screenshot, then wait for the loading or modal state to end. I would verify the target is visible and enabled before clicking. Forcing the click would skip evidence that a user cannot reach the control.
How do Cypress command retries differ from test retries?
Command retry-ability repeats eligible query and assertion chains within one attempt. Test retries rerun an entire failed test, including beforeEach hooks. A retry pass is evidence of intermittent behavior, not proof that the test is stable.
What do you check when a Cypress spec passes locally but times out in CI?
I reproduce with cypress run, inspect failure screenshots and logs, verify server readiness, and confirm the same URL and data are available to CI. In Docker I check hostname and network configuration from inside the test container. Only after finding the delay would I consider a scoped timeout.
Frequently Asked Questions
What does "Timed out retrying after 4000ms" mean in Cypress?
A retryable command or assertion did not reach its expected state within its configured timeout. The default command timeout is 4000 milliseconds, but the message alone does not reveal whether the selector, page state, or application behavior is wrong.
How do I increase the timeout for one Cypress element?
Pass `{ timeout: 10000 }` to a query such as `cy.get(selector, { timeout: 10000 })`. Choose the value from observed healthy latency and confirm the selector and prerequisite state first.
Why does cy.get time out when I can see the element later?
The element may appear after the query budget, or the test may have started before a required navigation or request. Assert the prerequisite and use a specific alias or UI readiness signal to understand the delay.
Should I use cy.wait(4000) to fix a Cypress timeout?
A fixed sleep does not prove that the page is ready and can fail under slower CI conditions. Wait on the relevant network alias or assert an observable UI condition instead.
Does defaultCommandTimeout control cy.wait with an alias?
Aliased network waits use requestTimeout while waiting for a request and responseTimeout while waiting for its response. Inspect which wait phase failed rather than assuming a DOM timeout applies.
Why does a Cypress timeout happen only in Docker?
The application may not be ready, or Cypress may be using localhost when the app is in another container. Verify the app URL from inside the Cypress container and use the service hostname on a shared network.
Will Cypress test retries solve a 4000ms failure?
Test retries rerun the failed test and may reveal intermittency, but they do not repair a bad selector or missing prerequisite. Track retry passes and fix the underlying race or data dependency.
Related Guides
- How to Fix "Cypress failed to start" and cypress verify Errors
- How to Fix Cypress "cy.visit() failed trying to load"
- How to Fix Playwright "Timed out waiting from config.webServer"
- How to Fix "Playwright Test did not expect test() to be called here"
- How to Fix "The Cypress binary is missing" in CI
- How to Fix Appium WebDriverAgent Failed to Start on iOS