Skip to content

Flaky Tests

A flaky test passes and fails on the same code. It is the single most destructive problem a test suite can have, because its damage is not limited to itself.

graph LR
    F[One flaky test] --> RR[People re-run to get green]
    RR --> HABIT[Re-running becomes the reflex]
    HABIT --> REAL[Real failures get re-run too]
    REAL --> ESCAPE[A genuine defect ships]
    HABIT --> TRUST[The whole suite stops being believed]

A suite with a few tolerated flaky tests is worse than a smaller suite with none, because it costs the same to run and provides no signal anyone acts on.

Causes, ranked by frequency

Cause Typical form Fix
Timing Fixed sleep waiting for something asynchronous Wait for a condition, with a generous timeout
Shared state Order dependence, leftover data Isolation: unique data or rollback
Real time Date arithmetic against the system clock Inject the clock
Concurrency Assertions on scheduling order Assert final state, not sequence
External systems Third-party sandbox flakiness Simulate locally, verify contracts separately
Resource limits Ports, memory, connection pools under parallel load Ephemeral resources per worker
Ordering assumptions Query results without an explicit sort Sort, or assert on membership
Test pollution in the code under test Caches and singletons retaining state Reset, or remove the global

Note that most of these are the test's fault, but not all. A test that fails under concurrency sometimes has found a real race condition, and deleting it hides a production defect. Investigate before concluding it is noise.

A policy that works

graph TD
    D[Detect: track pass and fail history per test] --> Q[Quarantine: remove from the gating path immediately]
    Q --> T[Ticket with an owner and a deadline]
    T --> FIX{Fixed by the deadline?}
    FIX -->|yes| BACK[Return to the gating suite]
    FIX -->|no| DEL[Delete it]

The two decisions that make this work:

  • Quarantine immediately. A flaky test in the gating path is worse than no test, so it leaves the gate the moment it is identified, without debate.
  • Delete on the deadline. A quarantine with no expiry is a graveyard, and a graveyard of disabled tests is indistinguishable from having deleted them, except that it also looks like coverage.

Detecting it automatically

  • Re-run failures once, and record it. Not to hide the failure, but to label the test as flaky in the history rather than silently passing it.
  • Track per-test pass rate over time. Anything below 100% on unchanged code is flaky by definition.
  • Run the suite repeatedly against an unchanged commit, nightly. Every failure in that run is flakiness, with no ambiguity.
  • Randomise order and run in parallel by default, so coupling surfaces early instead of after the suite has grown.

Measuring the problem

A single number is enough to make it manageable: the proportion of pipeline runs that fail for reasons unrelated to the change. Once that is visible, flakiness becomes a tracked defect rather than an ambient irritation everyone works around.

Check Your Understanding

#

Why is a flaky test worse than having no test at all?

#

Why must quarantine come with a deadline?