All insights
Quality engineering

Cypress Flaky Tests: Find the Real Cause

A Cypress test that fails on Tuesday and passes on Wednesday isn't random. Learn the three real causes of flaky tests and how to fix each one.

Nov 6, 20266 min read
Cypress Flaky Tests: Find the Real Cause — Tesbo

A QA lead on a 40 person engineering team knows the pattern well. The Friday deploy is blocked because checkout_spec.js failed in CI. Someone reruns the job. It passes, and everyone moves on without asking why. That single rerun costs 8 minutes of pipeline time. Multiply it by the 15 to 20 flaky reruns a mid sized suite triggers in an average week, and a team is quietly burning two or three engineering hours on tests that were never actually broken.

Cypress flaky tests almost never happen at random. The fix most teams reach for first is cy.get(selector, { retries: 3 }) or a global retry config bump. It works, in the sense that the pipeline goes green. But a retry is a bandage over a symptom, not a diagnosis.

This post walks through the three causes that actually produce most Cypress flakiness:

  • Timing assumptions that outrun the app
  • Shared state bleeding between tests
  • Test order dependencies that only pass in sequence

Each has a specific fix, so the retry count can go back down instead of up.

Why adding retries makes the problem worse over time

Every retry that passes on the second or third attempt trains the team to distrust the first result. Within a few months, a red X in CI stops meaning "something broke." It starts meaning "rerun it and see." That is a dangerous habit. The 2024 release where a genuine regression in the payment form got waved through as "probably flaky" is a story most QA leads have lived through once already.

That number should worry someone. Fixing the underlying cause is the only way to get the visible pass rate to match the real one.

Cause one: timing assumptions that outrun the app

The single most common source of Cypress flakiness is a test that asserts before the application has actually finished doing something. A cart total that updates via an API call, a modal that animates in over 300 milliseconds, a table that repaints after a websocket push. All of these can pass locally on a fast machine and fail in CI on a loaded runner.

The tell is a test that fails more often in CI than on a developer's laptop. It also fails more often when the whole suite runs than when the spec runs alone. That pattern points straight at timing, not at broken app logic.

The fix is to stop waiting for time and start waiting for state:

  • Replace cy.wait(500) with cy.get('[data-testid="cart-total"]').should('contain', '$42.00'), which polls until the assertion is true instead of guessing a duration.
  • Intercept the network call the UI depends on with cy.intercept() and cy.wait('@aliasedRequest') so the test waits for the actual response.
  • For CSS driven animations, assert on the end state, like visibility or a class name, rather than trying to catch the element mid transition.

A team that migrates a checkout spec from three cy.wait(1000) calls to state based assertions typically sees that spec's flake rate drop from a coin flip to near zero. The test is no longer racing the browser.

Cause two: shared state bleeding between tests

The second cause shows up when a test passes in isolation but fails when it runs after a specific other test. It can also fail only on the second run of the day. That is a strong signal that two tests are reading or writing the same database row, the same seeded user, or the same piece of localStorage.

A 900 test Cypress suite that takes 50 minutes is exactly the environment where this bites hardest. Dozens of specs compete for the same seeded fixtures. The practical fixes:

  • Give each test its own data, created in a beforeEach via API call rather than relying on a shared seed script.
  • Clear cookies, localStorage, and session storage between tests instead of assuming Cypress resets everything.
  • Avoid asserting on absolute counts, like "there are 5 items in the list," when other tests might add or remove items from that list. Assert on the specific item the test created instead.

Cause three: order dependent tests that only pass in sequence

Order dependent tests are related to shared state but distinct enough to call out separately. These are tests written to assume an earlier test left the application in a particular state, like being logged in or having a draft already saved. They pass fine until someone reorders the spec file, runs a single test with .only, or the CI runner parallelizes specs across machines.

This is the failure mode behind the classic bug report: it works alone, and in CI when run alone, but fails only when the full suite runs in parallel. Parallelization is increasingly the default in CI. A suite with hidden order dependencies will start failing more, not less, as a team scales its pipeline.

The fix is to make every test independently runnable:

  • Each it() block should set up its own preconditions rather than relying on a previous it() in the same file.
  • Use beforeEach for setup that must happen fresh every time, not before, which only runs once per file.
  • Run the suite with cypress run --spec on a single random test occasionally to catch hidden dependencies before they surface in CI.

How to tell which cause you're dealing with

Before touching any code, run the suspect test three ways: alone, as part of the full suite, and with --parallel if the CI setup supports it. A test that only fails in the third scenario points to order or shared state. A test that fails inconsistently even when run alone, especially in CI but not locally, points to timing.

Writing this quick triage down before assigning the bug to someone saves a repeat of the "I couldn't reproduce it" cycle. That cycle so often follows a flaky test report.

It also helps to track flake rate per test over a few weeks rather than reacting to single failures. A test that fails once in 50 runs might be genuine environment noise. A test that fails 3 times in 10 runs has a real, findable cause. It is worth the 30 minutes it takes to fix properly instead of the 5 minutes it takes to bump a retry count.

Questions people ask

Is it ever fine to use Cypress retries?

Retries are reasonable as a short term safety net for a known, already diagnosed issue, but they should not be the permanent fix for an undiagnosed test.

Why does a flaky test often pass locally but fail in CI?

CI runners are typically slower and more resource constrained than a developer laptop, which exposes timing assumptions that happen to hold locally.

How do I find which earlier test is causing a shared state failure?

Run the suite with .only on the suspect test to confirm it passes alone, then reintroduce specs one at a time until the failure reappears.

Does Cypress reset state automatically between tests?

It clears some things between tests, like the DOM, but not everything, such as localStorage or cookies set outside its own commands.

What's a quick way to check if a test is order dependent?

Run just that one spec file with cypress run --spec in isolation. If it passes alone but fails in the full suite, order or shared state is the likely cause.

Keep going

Try Tesbo, or get the next useful idea

Start building your testing workflow now, or get one practical email a month.

Start free

One email a month

What we shipped, what we learned, and the occasional infographic worth pinning. Unsubscribe in one click.