Cypress Test Retries: A Strategic Tool or a Band-Aid for Flakiness?

August 5, 2025

There's a familiar dread in the world of software development: the red 'X' on a CI/CD pipeline, signaling a failed build. The dread intensifies when, upon re-running the exact same job, it passes with a green checkmark. This is the frustrating reality of flaky tests—intermittent failures that erode confidence and grind productivity to a halt. In response to this pervasive issue, the Cypress team introduced a powerful feature: Cypress test retries. This built-in mechanism automatically re-runs a failing test, offering a potential lifeline to keep development pipelines flowing. However, this raises a critical question for engineering teams: are Cypress test retries a pragmatic solution for unavoidable instability, or are they merely a band-aid that covers up deeper, more sinister problems in your test suite? This comprehensive guide explores the mechanics, strategic applications, and inherent dangers of relying on test retries, providing a framework for using them as a strategic tool rather than a crutch.

Deconstructing the Enemy: What Causes Test Flakiness in Cypress?

Before we can properly evaluate the solution, we must first understand the problem. A 'flaky' test is one that can both pass and fail when run multiple times against the exact same code. This unpredictability is the primary source of frustration. While Cypress is designed with a retry-ability mechanism for its commands, which mitigates many common issues, flakiness can still creep in. A study by Microsoft Research highlighted that flaky tests are a significant drain on developer resources, making their identification and resolution a high-priority task.

Understanding the root causes is the first step toward building a more resilient test suite. The most common culprits include:

  • Asynchronous Operations: Modern web applications are fundamentally asynchronous. Tests often need to wait for API calls to complete, data to be fetched, or animations to finish. If a test tries to interact with an element before it's ready (e.g., a button that is disabled until a network request resolves), it will fail. A slow backend response on one run could cause a failure, while a fast response on the next could lead to a pass.

  • Race Conditions: These occur when the outcome of a test depends on the unpredictable sequence of two or more independent operations. For example, a test might assert that a list contains 10 items after a 'load more' button is clicked. If the assertion runs before all 10 items have been rendered in the DOM, the test will fail. The timing of the render and the assertion creates a race.

  • Environment Instability: The environment where your tests run is not always perfectly stable. Issues like network latency, database connection hiccups, or transient errors from third-party services can cause a test to fail. A subsequent run, free of these environmental blips, will pass. This is one area where Cypress test retries can be particularly justifiable.

  • Test Data Dependencies and State Pollution: The most robust tests are independent and isolated. Flakiness arises when one test inadvertently changes the application's state in a way that affects a subsequent test. For instance, if Test A creates a user but doesn't clean up after itself, Test B, which expects a clean slate, might fail. According to principles outlined by Martin Fowler, such non-determinism is a primary source of test suite decay.

  • Improper Waiting Strategies: A common anti-pattern for beginners is using arbitrary waits like cy.wait(5000). This is a recipe for flakiness. If the operation takes 5.1 seconds, the test fails. If it takes 2 seconds, you've wasted 3 seconds of execution time. The correct approach, as detailed in the official Cypress documentation, is to use assertions to wait for a desired state, such as cy.get('.my-element').should('be.visible').

How Cypress Test Retries Work: Configuration and Mechanics

Cypress provides a simple yet powerful configuration for implementing test retries. This feature can be enabled globally for all tests or specified for individual tests or test suites, offering a high degree of control. The core idea is to instruct the Cypress Test Runner to retry a test a given number of times only if it fails. If the test passes on any of its attempts, it is marked as passed for the entire run.

Global Configuration

You can set a global retry policy in your main configuration file, cypress.config.js (or .ts). This is the most common way to implement a baseline strategy for handling flakiness across your project. The configuration distinguishes between runMode (when running headlessly via cypress run) and openMode (when running interactively via cypress open).

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // other configuration...
  retries: {
    // Configure retry attempts for `cypress run`
    // Default is 0
    runMode: 2,
    // Configure retry attempts for `cypress open`
    // Default is 0
    openMode: 0
  }
})

In this example, any test that fails during a cypress run command will be retried up to two more times. If it fails three times in total, it will be marked as a failed test. By setting openMode to 0, we ensure that tests do not automatically retry during local development, which is a recommended best practice to avoid masking issues while writing tests.

Per-Test Configuration

For a more surgical approach, you can override the global settings for a specific test or suite. This is highly recommended for situations where you've identified a specific flaky test that is difficult to fix immediately but you don't want to apply retries to your entire suite.

You can configure this in the test's configuration object:

// In a spec file, like login.cy.js

describe('User Login Flow', () => {
  // This test will use the global retry configuration
  it('should display validation errors for empty fields', () => {
    // test logic
  });

  // This specific test will be retried up to 3 times upon failure
  it('should successfully log in with valid credentials', { retries: 3 }, () => {
    // test logic that might be flaky due to a slow backend
  });
});

This granular control is a key aspect of using Cypress test retries responsibly. It allows teams to contain the 'band-aid' to a specific, known issue while maintaining a zero-retry policy for the rest of the test suite. The official Cypress documentation on Test Retries provides extensive examples and is the definitive source for configuration options.

Furthermore, when used with the Cypress Dashboard, tests that passed after a retry are flagged as 'Flaky'. This creates an invaluable feedback loop, as noted in a Forbes Tech Council article on observability, allowing teams to track, quantify, and prioritize fixing the root causes of flakiness rather than just letting them pass silently.

The Argument for Retries: A Pragmatic Approach to CI/CD Stability

While it's easy to label retries as a lazy fix, there's a strong, pragmatic argument for their intelligent use. In the real world of software delivery, pragmatism often trumps perfection. The primary goal of a CI/CD pipeline is to provide fast, reliable feedback and enable continuous delivery of value. A single, low-impact flaky test should not be allowed to become a bottleneck for an entire development team.

Here’s the case for strategically using Cypress test retries:

  1. Maintaining Development Velocity: A blocked pipeline is a costly one. As highlighted in the DORA State of DevOps Report, elite performers deploy frequently and have low change failure rates. When a flaky test blocks a deployment, developers must stop their work, investigate the failure, and re-run the pipeline, costing valuable time and context. A single, automatic retry can resolve a transient environmental hiccup in seconds, keeping the pipeline moving and allowing the team to focus on building features.

  2. Handling Unavoidable Environmental Flakiness: Not all flakiness originates from your test code. Your application might rely on third-party APIs, experience momentary network congestion, or interact with a staging database that is under load. These are external factors that are often outside the test's control. In these scenarios, a retry is not a band-aid for bad code; it's a resilient strategy for handling an unstable environment. It acknowledges the reality that the world in which the test runs is not a perfect vacuum.

  3. A Tool for Data Collection and Triage: Perhaps the most powerful argument for retries is their ability to generate data. When a test passes after a retry, it's not swept under the rug—especially if you use a tool like the Cypress Dashboard. It gets flagged as 'flaky'. This transforms a frustrating, random failure into a concrete data point. Over time, you can build a report of your flakiest tests. This data-driven approach, a cornerstone of modern engineering according to research from McKinsey, allows teams to prioritize their efforts. Instead of guessing which tests are problematic, you can focus on fixing the ones that fail and retry most often.

  4. A Temporary, Controlled Stopgap: When a critical flaky test is discovered right before a release, there may not be time for a deep investigation. Applying a specific, documented Cypress test retry (e.g., { retries: 1 }) with an associated ticket in your issue tracker is a responsible, short-term solution. It unblocks the release while ensuring the underlying issue is tracked and scheduled for a proper fix. This controlled use prevents the accumulation of technical debt, a concept well-documented by sources like the IEEE.

The Dark Side of Retries: Masking Deeper Problems

For every argument in favor of retries, there is a compelling counterargument that warns of their dangers. The 'band-aid' metaphor is potent because, like a real band-aid, Cypress test retries can hide a festering infection underneath. Over-reliance on this feature can lead to a systemic decay of test suite quality and a culture of complacency.

The primary risks include:

  • Obscuring the Root Cause: This is the cardinal sin of misusing retries. A flaky test is a bug. It might be a bug in your application code (like a race condition) or a bug in your test code (like an improper wait). When a retry makes the test pass, the immediate pressure to fix the bug vanishes. The problem doesn't go away; it's just hidden. This creates a growing pool of technical debt that makes the system more fragile and harder to debug in the future. A report on the cost of technical debt suggests these hidden issues can eventually grind development to a halt.

  • Erosion of Confidence: A test suite's value is directly proportional to the team's trust in it. When developers know that tests only pass because they are retried two or three times, they begin to lose faith. A genuine failure—one that correctly identifies a regression—might be dismissed as 'just another flaky test'. This erosion of trust can lead to developers ignoring failing tests, defeating the entire purpose of an automated test suite. As testing advocate Kent C. Dodds often emphasizes, the goal of testing is to ship with confidence, a goal undermined by unreliable tests.

  • Increased Execution Time and Cost: Retries are not free. Each retry attempt re-runs the entire test from the beginning, including all beforeEach hooks. If you have a 5-minute test that is retried twice, you've just added 10 minutes to your CI pipeline. Across an entire suite, this can lead to significantly longer feedback cycles and higher costs for CI/CD minutes. Optimizing CI/CD performance is a key DevOps metric, and widespread retries work directly against this goal, as noted in best practices from providers like CircleCI.

  • The Slippery Slope of Normalization: What begins as a single, justified retry for one problematic test can easily become a global standard. A team might decide, 'Let's just set retries: 2 for everything to improve stability.' This is a dangerous path. It normalizes flakiness and discourages the disciplined practice of writing stable, deterministic tests from the outset. It shifts the team's mindset from 'let's fix this' to 'the retries will catch it,' creating a brittle and unreliable foundation for quality assurance.

A Strategic Framework for Implementing Cypress Test Retries

The consensus among seasoned engineers is that Cypress test retries should be used, but with extreme discipline. They are a powerful feature for managing real-world complexities, not for excusing poor test design. To use them effectively, teams should adopt a clear and strategic framework.

Here are actionable best practices for using Cypress test retries responsibly:

  1. Investigate First, Retry as a Last Resort: Every flaky test should be treated as a P1 or P2 bug. The default action should always be to investigate the root cause. Check for missing cy.intercept() calls, add assertions to wait for state changes instead of using fixed waits, and ensure proper test cleanup in afterEach hooks. Only after a concerted effort to fix the test fails should a retry be considered as a temporary measure.

  2. Isolate and Limit Retries to Specific Tests: Avoid global retries. The most responsible way to use this feature is with the per-test configuration. This method acts as a form of documentation, clearly marking a specific test as problematic.

    // Add a comment explaining WHY the retry is needed and link to a ticket.
    it('should load the user dashboard correctly', { retries: 1 }, () => {
      // TODO: Fix flakiness caused by slow-loading chart component. See TICKET-123.
      cy.get('[data-testid="dashboard-chart"]').should('be.visible');
    });
  3. Establish a 'Flaky Test Quarantine' Policy: Formalize the process. When a test is identified as flaky, it should be placed in 'quarantine'.

    • Step 1: Ticket Creation. A bug ticket is immediately created to track the investigation and fix.
    • Step 2: Apply a Temporary Retry. A single retry ({ retries: 1 }) is applied to the test, with a code comment linking to the ticket.
    • Step 3: Monitor. Use the Cypress Dashboard or other monitoring tools to track how often the quarantined test fails and retries. This data helps prioritize the fix.
    • Step 4: Prioritize Fixing. Allocate time in sprints or dedicated 'fix-it days' to clear the quarantine backlog. The goal is always to remove the retry. This process is supported by insights from engineering blogs like GitHub's, which stress the importance of structured processes for maintaining CI health.
  4. Focus on Writing Resilient Tests from the Start: The ultimate solution to flakiness is prevention. Invest in training your team on robust testing patterns. The Cypress Best Practices guide is an essential resource. Key principles include:

    • Control the Network: Use cy.intercept() to stub or mock API responses. This decouples your frontend tests from backend instability and makes them faster and more deterministic.
    • Assert on State, Not Time: Never use cy.wait(number). Instead, write assertions that wait for the UI to reflect the desired state (e.g., a button becomes enabled, a spinner disappears, an element contains specific text).
    • Ensure Test Isolation: Always clean up state created during a test. Use beforeEach hooks to reset the application state, ensuring every test starts from a known, clean slate. A look at the guiding principles of libraries like Testing Library reinforces this user-centric, state-driven approach.

Ultimately, Cypress test retries are neither an absolute good nor an absolute evil; they are a tool, and the value of a tool depends entirely on the skill and discipline of its user. Viewing them as a simple band-aid is a dangerous oversimplification that leads to technical debt and a brittle test suite. Conversely, rejecting them entirely can be an act of dogmatic purity that harms development velocity. The most effective approach is a balanced one: use Cypress test retries as a surgeon would use a temporary clamp—a precise, controlled, and short-term measure to stop a pipeline from bleeding out, while you prepare to perform the real surgery of fixing the underlying cause. By adopting a strategic framework, you can leverage their benefits for CI/CD stability without falling into the trap of masking deep-seated problems.

What today's top teams are saying about Momentic:

"Momentic makes it 3x faster for our team to write and maintain end to end tests."

- Alex, CTO, GPTZero

"Works for us in prod, super great UX, and incredible velocity and delivery."

- Aditya, CTO, Best Parents

"…it was done running in 14 min, without me needing to do a thing during that time."

- Mike, Eng Manager, Runway

Increase velocity with reliable AI testing.

Run stable, dev-owned tests on every push. No QA bottlenecks.

Ship it

FAQs

Momentic tests are much more reliable than Playwright or Cypress tests because they are not affected by changes in the DOM.

Our customers often build their first tests within five minutes. It's very easy to build tests using the low-code editor. You can also record your actions and turn them into a fully working automated test.

Not even a little bit. As long as you can clearly describe what you want to test, Momentic can get it done.

Yes. You can use Momentic's CLI to run tests anywhere. We support any CI provider that can run Node.js.

Mobile and desktop support is on our roadmap, but we don't have a specific release date yet.

We currently support Chromium and Chrome browsers for tests. Safari and Firefox support is on our roadmap, but we don't have a specific release date yet.

© 2025 Momentic, Inc.
All rights reserved.