ENGINEERING INSIGHTS · QUALITY ENGINEERING
Flaky tests are a signal, not noise.
How to detect flaky tests from CI data, quarantine them without losing coverage, and fix the usual causes: fixed waits, shared state, real clocks, and live network calls.
01 / 05
What a flaky test actually costs
A flaky test passes and fails on the same code. The first time it fails, someone investigates. The fifth time, someone clicks rerun. By the twentieth, the team has learned that a red build means nothing in particular, and a real regression can slip through on the third retry.
That loss of trust is the real cost, and it compounds. Engineers stop reading failures. Automatic retries are added to keep the pipeline green, which hides new flakiness as it appears. At Google's scale, about 1.5% of all test runs reported a flaky result, and almost 16% of tests showed some level of flakiness, according to its testing blog in 2016. You do not need that scale to feel it; a few hundred browser tests are enough.
A flaky test is also not always a test problem. Sometimes the nondeterminism is in the product: a race between two requests, a cache that serves stale data under load, a missing database constraint. Treating every flake as noise means occasionally ignoring the only evidence of a real bug.
02 / 05
Detect flakiness from data, not anecdotes
The definition gives you the detection method: a test is flaky if it both passed and failed on the same commit. If your CI keeps results per test (most runners can emit JUnit XML, and it is worth loading into a table), one query finds them:
-- Tests that both passed and failed on the same commit in the last 14 days.
SELECT test_id,
count(*) AS flaky_commits,
max(last_seen) AS last_seen
FROM (
SELECT test_id, commit_sha, max(finished_at) AS last_seen
FROM test_results
WHERE finished_at > now() - interval '14 days'
GROUP BY test_id, commit_sha
HAVING bool_or(passed) AND bool_or(NOT passed)
) AS mixed
GROUP BY test_id
ORDER BY flaky_commits DESC;Rank by the number of commits affected, not by raw failure count: a test that failed forty times on one broken branch is broken, not flaky. Rerunning failed tests automatically is a fine detection mechanism, as long as a pass on retry is recorded and reported as a flake and never silently counted as a pass.
03 / 05
Quarantine with an owner and an expiry date
Once a test is known to be flaky, it should stop blocking merges, because a gate that fails at random teaches people to bypass gates. It should not stop running. Quarantine means the test runs in a separate, non-blocking job, its results are still recorded, and it has an owner and a date by which it is fixed or deleted.
- 01Fails in CI
Merge blocked; failure recorded with the commit SHA
- 02Rerun on the same commit
Same code, fresh environment
Fails again: a real failure; fix before merging
- 03Recorded as flaky
Passed on retry, so it counts as a flake, not a pass
- 04Quarantined
Non-blocking job, a named owner, a fix-by date
Past its expiry: fixed or deleted, never silently renewed
- 05Fixed and returned
Root cause fixed, stable across repeated runs, back in the gate
With Playwright, tags and annotations carry the quarantine metadata in the test itself, where reviewers see it:
test("exports the monthly report", {
tag: "@quarantine",
annotation: [
{ type: "owner", description: "billing-team" },
{ type: "expires", description: "2026-11-15" },
{ type: "issue", description: "https://tracker.example.com/QA-482" },
],
}, async ({ page }) => {
// ...
});npx playwright test --grep-invert @quarantine # blocking job
npx playwright test --grep @quarantine # non-blocking job; results still storedA short script in CI that fails when any expires annotation is in the past keeps the list honest.
04 / 05
The usual causes, and what fixes them
Most flakiness comes from a short list of causes. An empirical study of flaky tests in open-source projects found the top three root causes to be asynchronous waits, concurrency, and test order dependency. It also found that most flaky tests (78%) were flaky from the moment they were written, which makes review and new-test stress runs the cheapest place to catch them.
| Cause | Typical symptom | Fix |
|---|---|---|
| Waiting for a fixed time | Fails on slow CI machines, passes locally | Wait for an observable condition, never a fixed sleep |
| Shared or leftover state | Fails only alongside other tests, or in a certain order | Each test creates its own data and never depends on another test's output |
| Real clocks and time zones | Fails at midnight, at month end, or on a runner in another zone | Inject or control the clock, and pin the time zone |
| Live network dependencies | Fails when a third-party sandbox is slow or down | Stub external services at the boundary; test the contract separately |
| Unordered results | Fails when the database returns rows in a different order | Assert on sets, or add an explicit ORDER BY in the code under test |
| Resource leaks | Fails late in long runs, more often with more parallel workers | Close connections and pages in teardown; cap workers to what CI can hold |
| A race condition in the product | Fails rarely, in a way that also appears in production logs | Fix the product, and keep the test as the regression check |
Wait for conditions, not time
The most common fix is the simplest. A fixed sleep is a guess about how long something takes, and CI machines are slower and noisier than laptops. Wait instead for the condition a user would see. Playwright's web-first assertions do this by default: they retry until the condition holds or the timeout expires.
// Flaky: assumes saving always finishes within two seconds.
await page.click("#save");
await page.waitForTimeout(2000);
expect(await page.textContent(".toast")).toBe("Saved");
// Deterministic: waits for what the user actually sees, up to the assertion timeout.
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("status")).toHaveText("Saved");Control the clock
Anything that depends on the current time, such as expiry banners, "due today" badges, or reports for last month, will eventually run on a day that breaks it. Control time explicitly. In unit tests, pass a clock into the code under test. In browser tests, Playwright's clock API fixes the time the page sees, and setting timezoneId in the test configuration stops the runner's zone from changing the result.
test("shows the renewal banner seven days before expiry", async ({ page }) => {
// The fixture account's subscription renews on 2026-03-31.
await page.clock.setFixedTime(new Date("2026-03-24T09:00:00Z"));
await page.goto("/account");
await expect(page.getByRole("alert")).toContainText("Renews in 7 days");
});05 / 05
A team policy that keeps the suite honest
- A test that passes on retry is a flake, recorded and visible, never a silent pass.
- A known flaky test leaves the blocking gate within a day and keeps running in quarantine.
- Every quarantined test has an owner and an expiry date, and an expired quarantine fails the build.
- Each fix records its root-cause category, so recurring causes become engineering work rather than individual chores.
- The flake rate is reviewed with build time and failure rate as a measure of the suite's health.
The aim is not zero flaky tests forever. It is a suite where red means something, so that when it fails, people read it. That is the standard we hold test automation work to.
References
- Flaky Tests at Google and How We Mitigate Them Google Testing Blog, 2016
Flake rates across a very large test corpus, and the mitigations Google used.
- An Empirical Analysis of Flaky Tests Luo, Hariri, Eloussi, and Marinov, FSE 2014
Root causes and fixes of flaky tests across open-source projects.
- Eradicating Non-Determinism in Tests Martin Fowler, 2011
Quarantine, isolation, asynchrony, and time as sources of non-determinism.
- Assertions Playwright documentation
Auto-retrying, web-first assertions.
- Auto-waiting Playwright documentation
The actionability checks Playwright performs before each action.
- Clock Playwright documentation
Controlling time in browser tests.
HOW NEURALSHARKS CAN HELP
Related NeuralSharks services
- Engineering
Web development
Fast, accessible web experiences built for real use.
- Information architecture and interface development
- Responsive applications and content systems
- Performance and accessibility checks
- Quality
Software testing
Understand product risk before your users encounter it.
- Risk assessment and test planning
- Functional and exploratory testing
- Defect analysis and release reporting
- Quality
Test automation
Reliable feedback for every meaningful change.
- Automation strategy and framework setup
- API and critical journey test suites
- Pipeline integration and test maintenance