Flaky Test
A flaky test passes and fails on the same commit. The mechanisms behind flakiness, how to measure a flake rate, and how to confirm one in GitHub Actions.
A flaky test is a test that passes on one execution and fails on the next against the same code, with nothing in the source changing between the two. The result is nondeterministic because the test depends on something the code under test does not control: elapsed time, state that another test left behind, or the order the suite happened to run in.
Flakiness is a property of the test itself. A busier or slower machine changes how often a latent flake surfaces, so one suite can look stable on a laptop and unstable in GitHub Actions while the defect sits in the test the whole time.
Definition
A test is flaky when repeated executions against an identical commit, an identical dependency lockfile, and an identical environment produce both pass and fail outcomes. That wording sets the boundary on either side. A test that fails on every execution is a failing test and points at a real defect. A test that passes on every execution is a passing test. Flakiness occupies the region between those two, and it is measured as a rate rather than reported as a state.
The flake rate of a test is the fraction of executions on one commit that fail. A test that fails 3 executions out of 100 has a flake rate of 3 percent. Because the rate is a probability, one green re-run says almost nothing about the test, and one red run establishes only that the rate sits above zero.
The three usual mechanisms
| Mechanism | What the test depends on | Signature across runs |
|---|---|---|
| Timing | An event completing inside a fixed wait, a scheduler slice, a clock rollover, or a timeout | The failure rate rises with machine load, and a longer wait makes the failure disappear |
| Shared state | A fixture, database row, temp file, global mock, port, or environment variable that another test also touches | The failing test name moves between runs, and the test passes when executed alone |
| Order dependence | Setup performed by a test that happens to run earlier in the sequence | The suite passes in declaration order and fails once the order is shuffled or sharded |
Timing. The test asserts on a condition that becomes true shortly after it looks. Any suite carrying a fixed sleep already holds this defect, because the sleep hides it on an idle machine and exposes it on a loaded one. Real clocks add their own variants, including tests that break at a month boundary or across a daylight saving change.
Shared state. Two tests read and write the same thing. The write from one lands between the write and the assertion of the other. Parallel execution turns this from a rare event into a frequent one, because work that used to happen in sequence now overlaps.
Order dependence. A test relies on setup performed by a test that runs before it. The suite stays green while the ordering holds and turns red as soon as a framework shuffles the sequence, shards the suite across workers, or somebody deletes the earlier test.
Calls to services outside the repository sit slightly apart from those three. A rate limit, a DNS failure, or a 502 from a third-party host produces the same pass-and-fail pattern, and the repair is a fake or a recorded response rather than a change to the test's own logic.
Why a single re-run is weak evidence
Re-running is the cheapest response to a red build and the weakest evidence about it. Repetition is what turns a rate into a number. The table below assumes independent executions and gives the chance a suite survives a fixed number of repetitions.
| Flake rate per execution | Chance one re-run is green | Chance 30 repetitions are all green |
|---|---|---|
| 1 percent | 99 percent | 74 percent |
| 5 percent | 95 percent | 21 percent |
| 10 percent | 90 percent | 4 percent |
| 20 percent | 80 percent | 0.1 percent |
Two readings fall out of that arithmetic. A green re-run is the expected outcome at every rate in the table, so it carries very little information on its own. And 30 repetitions surface a 5 percent flake about four times out of five, which is why a repetition matrix is the standard way to confirm one.
Example
The clearest flake to reproduce is a shared fixture under parallel execution. This workflow runs a Python suite across four workers:
name: test
on: [push]
jobs:
unit-tests:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pytest -n 4 tests/Two tests in that suite share one session-scoped fixture, and each resets the balance on it before acting:
# tests/conftest.py
@pytest.fixture(scope="session")
def account():
return Account.objects.get_or_create(id=1)[0]
# tests/test_billing.py
def test_charge(account):
account.set_balance(100)
charge(account, 40)
assert account.balance == 60
# tests/test_refunds.py
def test_refund(account):
account.set_balance(100)
refund(account, 25)
assert account.balance == 125With a single worker both tests pass every time, because each one resets the row, acts on it, and asserts before the next test starts. Add -n 4 and the two tests execute at the same moment against the same row. When test_charge calls set_balance(100) after test_refund has already applied its refund, the refund assertion reads 100 instead of 125:
FAILED tests/test_refunds.py::test_refund - assert 100 == 125Which of the two reports the failure depends on which one loses the race, so the failing test name moves between runs. That is the shared-state signature from the table above, and it is the detail that separates this from a genuine defect in the refund code.
Two checks confirm the diagnosis. Running the file on its own with pytest tests/test_refunds.py passes on every attempt, which clears the refund logic. Running the whole suite with pytest -n 0 tests/ passes as well, which places the cause in the parallel execution rather than in either assertion.
Confirming the rate with a repetition matrix
A matrix runs the same commit many times and reports every result. That needs fail-fast set to false, so one red combination leaves the remaining combinations running instead of cancelling them (GitHub workflow syntax reference, checked on 2026-08-13):
jobs:
repeat:
strategy:
fail-fast: false
matrix:
attempt: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pytest -n 4 tests/test_billing.py tests/test_refunds.pyTen attempts against a suite whose per-execution flake rate is 20 percent come back all green about 11 percent of the time, since 0.8 raised to the tenth power is 0.107. Ten attempts are therefore plenty for an obvious flake and thin for a rare one. Widen the matrix until attempts multiplied by the estimated rate predicts at least a few failures.
The repair for this particular case is to stop sharing the row. Give each test its own record, scope the fixture to the function instead of the session, or key the record on the worker ID that the parallel plugin exposes to each process.
Related Terms
- Find and fix flaky GitHub Actions jobs: how to rank jobs by success rate, tell resource starvation apart from genuine nondeterminism, and open a shell on the machine that produced the failure.
- Fail fast in a GitHub Actions matrix: the matrix setting that cancels the remaining combinations after the first failure, and the reason a repetition matrix turns it off.
- How to retry a failed step in GitHub Actions: the retry options available at the step level and what a retry hides about the underlying rate.
- GitHub workflow syntax reference for strategy.fail-fast: the documented behavior of the matrix setting used in the repetition example.
- WarpBuild reports documentation: success rate, duration percentiles, and queue time percentiles per repository, workflow, and job name.
- Action-Debugger documentation: the open source action that pauses a workflow on a step and opens an SSH session on the runner.
- WarpBuild pricing: per minute rates by runner type.
FAQ
How is a flaky test different from a broken test?
A broken test fails on every execution against the same commit and points at a defect in the code or in the assertion. A flaky test produces both outcomes on that same commit, so it is described by a rate rather than by a state. A test that fails 3 times in 100 executions has a flake rate of 3 percent.
Does running tests in parallel cause flakiness?
Parallel execution exposes flakiness that was already present. Shared state and order assumptions are defects while the suite runs serially too, and parallelism widens the window in which two tests touch the same thing at once. Running the same suite with a single worker is the fastest check for whether shared state is the cause.
How many times should I re-run a test to confirm it is flaky?
Enough repetitions that the expected number of failures is at least a few, which is repetitions multiplied by the estimated flake rate. Thirty repetitions of a test that fails 5 percent of the time come back all green only about 21 percent of the time, so thirty attempts catch that test roughly four times in five.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.