This episode dissects Cypress's automatic waiting: how to wait for elements without manual sleeps, retry-ability on commands and assertions, handling timing issues, and best practices for reliable, non-flaky tests.

One of the main reasons people switch from Selenium to Cypress: no more random sleep calls scattered everywhere. Episode 5 explains why. You will understand automatic waiting, retry-ability on commands and assertions, and how to avoid patterns that actually make tests slow and flaky.
The understanding in this episode is what separates tests that live in a team forever from tests that get deleted because they fail too often.
Cypress doesn't execute commands blindly. When a command can't succeed immediately — for example, an element hasn't appeared yet because a fetch is still running — Cypress waits and retries until it succeeds or times out.
cy.visit("/dashboard");
cy.get("[data-cy=chart]").should("be.visible");
cy.get("[data-cy=loading-spinner]").should("not.exist");cy.get("[data-cy=chart]") will wait until the element appears in the DOM, while .should("not.exist") waits until the spinner is truly gone. No cy.wait(3000) — Cypress knows when the condition is met.
Automatic waiting doesn't run indefinitely. The limit is set by defaultCommandTimeout, which is 4000 milliseconds by default. You can adjust it per command:
cy.get("[data-cy=analysis-result]", { timeout: 15000 }).should("be.visible");{ timeout: 15000 } extends the wait for this command alone to 15 seconds — useful for genuinely slow processes like computations or large loads.
The classic pattern that creates flakiness is guessing at time:
cy.wait(5000);
cy.get("[data-cy=result]").should("be.visible");cy.wait(5000) waits for a guessed number. Enough on a fast machine, not enough on a slow one. Replace that pattern with an assertion that waits until the condition is met:
cy.get("[data-cy=result]").should("be.visible");For asynchronous loading with a clear source, wait for the network event captured by cy.intercept() — not a time guess. We will dig into this in episode 12.
Be careful with elements that appear and then disappear quickly. Waiting for them to exist and then expecting them to still be there on the next action can be risky. It's safer to wait for the final condition, such as the result content becoming visible, rather than an intermediate element.
Not all commands are retried. Those that can be: cy.get, cy.contains, cy.find, cy.should, and assertions. Those that are not retried: cy.visit, cy.request, cy.then, and actions like cy.click — actions are only run once against the element already found.
cy.get("[data-cy=title]").should("have.text", "Welcome");
cy.wrap([1, 2, 3]).should("have.length", 3);.should("have.text", "Welcome") is retried: Cypress re-reads the element's text until it matches or times out. Likewise cy.wrap(...).should(...) — assertions inside should are rerun automatically.
For more complex logic, use a callback. Important: the callback must use expect, not assert, for retry to work:
cy.get("[data-cy=product-list]").should(($items) => {
expect($items).to.have.length(8);
});expect($items).to.have.length(8) inside the callback lets Cypress retry the entire callback until the assertion passes. This is the correct way to validate several elements at once without guessing at render time.
The fewer timing assumptions, the more stable the test. Prioritize:
cy.intercept() and cy.wait("@alias") for network flows.defaultCommandTimeout to what your app needs, don't force a short timeout.cy.wait(ms) with guessed numbers.it("displays search results", () => {
cy.intercept("GET", "/api/search*").as("search");
cy.get("[data-cy=query]").type("book");
cy.get("[data-cy=search]").click();
cy.wait("@search");
cy.get("[data-cy=results]").should("have.length.gt", 0);
});Here cy.wait("@search") waits for the API response to finish, then the assertion ensures the results are actually rendered. There isn't a single random sleep — every step waits until its condition is genuinely true.
Episode 5 changed the way you think about timing: Cypress waits for conditions, not for guessed time. Automatic waiting works through retry-ability — element-finding commands and assertions are retried until they succeed or time out. Timing issues are handled by waiting for the final condition, not random cy.wait(ms).
The key takeaways:
defaultCommandTimeout (default 4000 ms) bounds the wait; it can be overridden per command.cy.visit.cy.wait(ms) with guessed numbers is a source of flakiness — avoid it.cy.intercept() for async flows.In the next episode, episode 6, we will cover fixtures and test data — managing test data with fixtures, loading JSON via cy.fixture(), data-driven testing with external datasets, and mocking data and request stubs. Your tests will start separating data from logic.