Managing Flaky Tests

A flaky test is defined as a test that both passes and fails on the exact same version of the codebase without any modifications to the code or the test itself. While a single flaky test might sound like a minor, easily ignorable annoyance, a handful of them quickly becomes a compounding, systemic tax on the entire engineering organization. When a test suite routinely features intermittent failures, the core promise of continuous integration—that a red build means a broken application—quietly dies. Development teams inevitably condition themselves to ignore the red build, reflexively clicking the "re-run" button without investigating the root cause. This normalization of deviance ensures that real, catastrophic regressions are eventually shipped directly through a wall of ignored red alerts.

Flakiness is fundamentally a reliability-engineering problem, a distributed systems challenge, and an economic sinkhole. It is not merely an annoyance that can be solved with a retry button. Addressing flaky tests requires a rigorous taxonomy, systematic reproduction strategies, architectural fix patterns, and unforgiving CI policies that collectively preserve engineering trust in the test suite.

The Economic and Mathematical Reality of Flakiness

To understand why flaky tests cannot be tolerated, we must view them through the lens of probability and cost. At a small scale, a flaky test might waste a few minutes. At scale, the math becomes brutal.

Consider a test suite comprising N independent tests, where each test has an average probability p of exhibiting a flaky failure during any given run. The probability of the entire build succeeding is the probability that every single test passes. We can express the probability of a build failure as:

\begin{aligned} P(\text{Build Success}) &= \prod_{i=1}^{N} (1 - p_i) \\ P(\text{Build Failure}) &= 1 - \prod_{i=1}^{N} (1 - p_i) \\ &\approx 1 - e^{-\sum_{i=1}^{N} p_i} \end{aligned}

If we assume a uniform flake rate p = 0.0001 (one failure in ten thousand runs) across a suite of N = 10,000 tests, the probability of a given build failing purely due to flakiness is approximately 1 - (1 - 0.0001)^{10000} \approx 63.2\%. This means that a seemingly phenomenal individual test reliability rate of 99.99% still results in the majority of your CI pipelines turning red.

The economic cost is equally staggering. Consider a conservative scenario where a single flaky test causes a build to fail, requiring an engineer to wait an additional 15 minutes for a re-run. If this happens 20 times a day across a large engineering department, that represents 5 hours of idle or context-switching time daily. Over a standard working year, this equates to 1,250 hours. Assuming a fully loaded engineering cost of $150 per hour, a single, moderately frequent flaky test costs the organization roughly $187.5K annually. For enterprises with deeply compromised test suites, the aggregate loss in productivity and compute resources can easily exceed $1.5M to $3.0M per year. Fixing flaky tests is not just about engineering hygiene; it is a high-ROI financial imperative.

The Taxonomy of Flaky Tests

Almost every flaky test originates from one of six structural causes. Correctly identifying the class is the prerequisite to applying the correct fix pattern:

  1. Async Timing and Race Conditions: This is the most prolific source of flakiness. The test asserts a state before the system under test has finished computing it. This manifests as tests racing against variable-latency operations: a UI assertion attempting to read the DOM before the frontend framework has finished rendering, a consumer attempting to read from a message queue before the broker has delivered the message, or an API client asserting a 200 OK before the database has finished writing the record.
  2. Test-Order Dependence: Test B passes when run in isolation, but fails if it is executed immediately after Test A. This occurs when tests share state and fail to clean up after themselves. Examples include leaving records in a shared database, modifying global configuration singletons, or altering environment variables. This class of flake remains invisible until a CI runner parallelizes the suite or randomly reorders the test execution schedule.
  3. Shared Mutable State in Parallel Execution: When modern test runners parallelize execution to speed up feedback loops, tests that previously ran sequentially now run concurrently. If Test A and Test B both attempt to mutate the same database row, bind to the same network port, or write to the same temporary file, they will collide. The failure depends entirely on the microsecond timing of the thread scheduler.
  4. External Dependencies: Tests that rely on systems outside their control—such as third-party payment APIs, external DNS resolution, or public internet connectivity—will inevitably fail when those external systems experience downtime, latency spikes, or rate limiting. These failures represent environmental chaos rather than application bugs.
  5. Concurrency in the Code Under Test: This is the most precious and dangerous class of flake. The test itself is perfectly written, but the production code has a genuine race condition, deadlock, or thread-safety bug that only surfaces probabilistically. These are real, user-facing bugs masquerading as flaky tests.
  6. Resource Exhaustion and Sensitivity: Tests that run perfectly on an idle, high-powered developer workstation might fail on heavily loaded, CPU-throttled CI runners. Strict timeouts that are easily met on a MacBook Pro will trigger falsely on a shared CI node. Similarly, tests sensitive to memory pressure, leap seconds, timezones, or execution across midnight boundaries fall into this category.

Advanced Reproduction Strategies

A flaky test that cannot be reproduced locally is effectively folklore. Attempting to fix a flake by guessing the cause and pushing to CI is an exercise in futility. The standard levers for forcing a flake to reproduce, ordered by effort, include:

Fix Patterns by Class

Once the root cause is isolated, structural fix patterns must be applied. Hacking a solution usually introduces a different form of flakiness.

Quarantine Workflows and CI Policies

While engineers work to fix flaky tests, the CI pipeline must remain a reliable gatekeeper. Two primary mechanisms allow CI to stay useful, provided they are managed with extreme discipline.

First is the Quarantine Workflow. A known flaky test can be tagged as "quarantined," meaning it will continue to execute during CI runs to gather telemetry, but its failure will not turn the overall build red. However, a quarantine without eviction pressure is simply a graveyard where test suites go to die. To prevent this, quarantine entry must be strictly regulated: adding a test to the quarantine must require an attached Jira/Linear ticket and an assigned owner. Furthermore, the quarantine list must have a hard time-to-live (TTL). If a test remains in quarantine for more than 14 to 30 days without being fixed, the test should be outright deleted, or the build should begin failing unconditionally.

Second is the controversial practice of Automatic Reruns (retry-on-failure). Tools like pytest-rerunfailures or Gradle's Test Retry can automatically re-execute a failed test. While this smooths over immediate CI blockages, it is immensely dangerous if not instrumented correctly. If a test fails once, passes on the second attempt, and the CI reports a "Green" build, the organization is blind to its declining suite health. The strict rule is that a pass-on-retry must be explicitly logged to a metrics backend as a flake event. The retry buys the developer an unblocked merge; the emitted metric buys the engineering organization the data needed to force a fix later.

Furthermore, gate policies for new tests should be merciless. Newly merged tests should be granted zero retry allowances for their first week of life. If a newly authored test cannot pass 50 consecutive runs deterministically, it should be rejected at the pull request stage.

Flake Analytics and Tracking

What gets measured gets fixed. Engineering organizations must treat flake rate as a top-tier operational metric, alongside API latency and error rates. By emitting per-test outcomes to a centralized ledger (using tools like Develocity, Datadog CI Visibility, or simple JUnit XML parsers writing to BigQuery), you can track suite health over time.

Crucially, flakiness almost always follows a Pareto distribution: out of 5,000 tests, a mere five to ten tests will typically be responsible for 80% of the red builds. By querying the metrics ledger for the most frequent offenders, you transform the vague, demoralizing sentiment that "our tests are flaky" into a precise, two-day, high-priority fix list for the platform team.

In conclusion, managing flaky tests is a continuous operational discipline. By embracing mathematical realities, isolating state, abolishing sleeps, and weaponizing CI analytics, organizations can reclaim trust in their test suites and prevent the hemorrhaging of millions in lost engineering capital.

See Also