June 15, 2026
How to Make Flaky Tests Less Expensive Without Hiding Real Release Risk
A practical analysis of flaky test cost, retry policies, quarantine tradeoffs, and ownership models that reduce noise without masking real release risk.
I have never met a team that enjoys flaky tests, but I have met plenty that tolerate them because the alternative feels worse. A noisy suite slows merges, burns engineering time, and makes people distrust automation. At the same time, a blunt fix like aggressive retries or sweeping quarantines can quietly erase the very signal that helps you catch regressions before they ship.
That is the real tension behind flaky test cost. The cost is not just a few reruns in CI. It shows up as delayed releases, triage meetings, broken trust in test results, and the hidden tax of having engineers babysit a pipeline instead of shipping product. The trick is not to eliminate every flake at any price. The trick is to make flakes cheaper to live with while preserving enough sensitivity to real release risk.
The economics of a flaky test
A flaky test is a test whose result changes without a corresponding product change. In practice, that means a test might fail because of timing, environment instability, shared state, infrastructure hiccups, nondeterministic ordering, or a genuine product bug that only appears intermittently. The software testing literature often treats this as a reliability problem, but in day-to-day engineering it is also an economics problem.
Every flaky failure has several costs:
- Developer interruption cost, someone stops their work to inspect a test failure.
- Triage cost, someone decides whether the failure is real, environmental, or just a transient blip.
- Pipeline cost, reruns consume CI minutes, compute, and queue time.
- Delay cost, the team waits longer for a trustworthy signal.
- Trust cost, people start ignoring results if noise becomes normal.
- Maintenance cost, engineers patch tests, add waits, add logging, or rewrite unstable flows.
When leaders talk about automation, they often focus on the cost of writing tests. That is only the beginning. The long-term test automation bill is dominated by keeping those tests reliable as the system evolves.
A useful mental model is this: a flaky test is expensive in proportion to how often it blocks decisions. A flaky nightly test that nobody watches is annoying. A flaky smoke test that blocks every merge is a production-level liability.
The expensive part of flakiness is not the failure itself, it is the uncertainty it injects into release decisions.
Separate annoyance from risk
Not every flaky test deserves the same response. Before changing policy, classify the failure by what it risks.
1. Pure noise
These failures do not indicate product instability. Common examples include:
- DOM timing issues in UI automation
- race conditions in test setup
- stale test data or shared accounts
- brittle selectors
- environment-specific network hiccups
These should be reduced, because they are pure automation maintenance cost.
2. Hidden product risk
Some flakes are actually early warnings. For example, a test that fails only under certain load patterns may point to a real concurrency bug. A test that passes after a retry could still be surfacing a race condition worth fixing.
These should not be buried behind unconditional retries or long-lived quarantines.
3. Ambiguous signal
This is the most dangerous category. The team does not know whether the failure is noise or a product problem. If your process forces an immediate binary decision, people will invent shortcuts. That is how retries become policy, and policy becomes denial.
The goal is not to force every flaky failure into a neat bucket. The goal is to make the default handling proportional to the risk of hiding a genuine regression.
Why retries feel cheap, and why they are not
Retries are often presented as a harmless stabilizer. They do reduce the visible flake rate, and sometimes that is exactly what you want for low-value signal. The problem is that retries also change the meaning of the pipeline.
A retry policy can do three things at once:
- Reduce false negatives from transient infrastructure problems.
- Mask test instability that should be fixed.
- Mask intermittent product defects.
The first is acceptable. The second is a debt transfer. The third is dangerous.
A practical way to reason about retry cost is to ask three questions:
- What class of failure is this retry meant to absorb?
- What evidence do we lose if the first failure disappears?
- How often will this retry hide something we care about?
If you cannot answer the last question, the retry policy is too broad.
A better retry policy
Use retries selectively, and only with observability. A good pattern is:
- retry once, not endlessly
- retry only for known transient failure signatures
- preserve the first failure details
- record whether the pass came on attempt 1 or attempt 2
- surface repeated retries as a separate health signal
Example in Playwright:
import { defineConfig } from '@playwright/test';
export default defineConfig({ retries: 1, use: { trace: ‘on-first-retry’ } });
This is not a cure. It is a compromise. A single retry can reduce noise from a flaky browser or infrastructure hiccup, while still keeping the failure visible enough for triage. What it should not do is make a broken test suite look healthy.
If your pipeline passes only after retries in a meaningful percentage of runs, that is not stability. That is a queue of unresolved problems.
Quarantine is a control, not a solution
Quarantining a flaky test means removing it from the critical path so it no longer blocks release decisions. That can be the right move, but only if quarantine is treated as a temporary risk containment measure, not a place where tests go to disappear.
Quarantine reduces short-term friction, which is why teams reach for it. It also creates an accountability vacuum if nobody owns the return path.
When quarantine makes sense
Quarantine is reasonable when:
- the test is not reliable enough to block release decisions
- the failure mode is known and tracked
- the product risk is covered by other tests or manual checks
- there is a clear owner and an exit criterion
When quarantine becomes a trap
Quarantine is a trap when:
- failures are not labeled by root cause
- the same test stays quarantined indefinitely
- no one reviews quarantine age
- the team uses quarantine to avoid fixing unstable infrastructure
- release managers assume quarantined tests have no value
A quarantined test still carries signal, it just should not be used as a gate. If you ignore quarantined tests entirely, you lose the ability to see whether the underlying problem is getting worse.
Quarantine should buy time to fix a problem, not permission to forget it exists.
Make quarantine measurable
Track at least these fields:
- test name
- first quarantine date
- owner
- suspected cause
- whether it blocks a critical flow
- target removal date
- number of reruns or observed failures while quarantined
This turns quarantine from a vague exception into an explicit risk ledger.
Ownership policies matter more than most dashboards
A flaky test without ownership will remain flaky. A flaky test with too many owners will also remain flaky. The problem is not just technical, it is organizational.
For automation to stay healthy, someone needs to own the test as a product artifact, not as disposable CI debris. That owner should understand the business flow, the implementation details, and the failure history.
Good ownership policies usually answer these questions:
- Who triages failures first?
- Who fixes the test if the issue is in automation?
- Who fixes the application if the issue is real?
- Who decides whether the test should be quarantined?
- Who approves returning it to the critical path?
A simple ownership model
For larger teams, I prefer this model:
- Feature team owns product correctness, including genuine failures.
- Automation engineer or SDET owns test mechanics, including locators, waits, and test data patterns.
- Release manager or QA lead owns gating policy, including which failures block release.
This separation avoids a common anti-pattern where one engineer has to decide both whether a product bug exists and whether the test is trustworthy. Those are different problems.
Flake triage should be a workflow, not a detective story
If every flaky failure becomes an ad hoc investigation, the organization will eventually stop investigating. That is a process smell, not a people problem.
A good flake triage process should collect enough evidence to make the next decision cheaper than the last one.
Capture the minimum useful evidence
At a minimum, capture:
- test name and suite
- commit SHA and branch
- environment and browser version
- first failure stack trace
- screenshots or video for UI failures
- network logs or API logs if relevant
- whether the test passed on retry
- recent code or data changes around the affected flow
For browser automation, this often means enabling traces, screenshots, and console logs. In Playwright, traces are especially useful because they show the sequence of actions leading to failure. In Selenium, the equivalent usually requires more custom logging, which is why some teams invest heavily in wrappers and reporters.
Standardize failure labels
Do not leave every failure as “flaky.” That label is too broad to drive action. Use categories such as:
- timing issue
- selector instability
- data dependency
- environment instability
- concurrency race
- product defect
- unknown
This helps you spot whether the cost is driven by one bad test, a bad subsystem, or a systemic reliability issue in the environment.
Use the cheapest fix first
Flake triage should start with the least invasive explanation that fits the evidence.
Examples:
- If the selector is brittle, fix the selector.
- If the test depends on old state, isolate the data.
- If the app needs time to render, wait for a real condition, not a sleep.
- If the environment is unstable, fix the environment before adding more retries.
This sounds obvious, but many teams do the opposite. They add retries to a test that is already telling them exactly what is wrong.
Reduce automation maintenance cost at the source
The best way to lower flaky test cost is to prevent flakiness from entering the suite in the first place.
1. Prefer deterministic waits over fixed sleeps
Fixed sleeps are one of the easiest ways to inflate maintenance cost. They may make a test pass locally, but they also make the suite slower and still fragile.
Playwright example:
typescript
await page.getByRole('button', { name: 'Save' }).click();
await page.getByText('Saved successfully').waitFor();
This waits for a real product state, not a guessed delay.
Selenium example in Python:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, ‘.toast-success’)) )
The engineering principle is the same, use observable conditions, not time assumptions.
2. Use stable selectors
UI tests become flaky when they depend on DOM structure instead of intent. Prefer accessibility roles, labels, stable data attributes, or other selectors intentionally designed for testing.
A test that clicks .container > div:nth-child(3) > button is announcing its own future failure.
3. Isolate test data
Shared test data is a major source of non-determinism. If multiple tests mutate the same account, order matters. If a test assumes a record is empty, then another test that ran earlier can invalidate that assumption.
Use unique data per run when possible, or reset state between runs. For integration suites, consider seeding through APIs or fixtures instead of relying on manually curated accounts.
4. Minimize cross-test coupling
Tests should be able to run alone and still pass. If one suite depends on another suite having created a user, that dependency is a hidden source of flakes.
5. Test the right layer
Not every user flow needs to be a browser test. If a flow can be validated at API or component level, you may lower flake risk by moving the assertion down a layer and keeping only a few end-to-end checks for full-path confidence.
That is not reducing coverage, it is redistributing it more intelligently.
How to decide whether a flaky test should block release
Release risk is not just about test count. It is about what kind of failure a test can catch, how likely that failure is, and how much damage would happen if it escaped.
A useful decision grid looks like this:
Block release if the test has all of these traits
- covers a high-value business flow
- has a history of catching real defects
- fails in a reproducible way when the product is broken
- is not currently known to be unstable for automation reasons
Do not block release if the test has these traits
- redundant with stronger signal elsewhere
- historically noisy and under active remediation
- tied to a non-critical path
- already covered by another reliable gate
Consider a soft gate if the test has mixed traits
A soft gate can alert, report, or require review without fully blocking deployment. This is often appropriate for suites that still provide signal but are not yet reliable enough for hard enforcement.
The risk is obvious, soft gates can be ignored. So if you choose them, ensure they are visible and time-bound.
If a flaky test is important enough to watch, it is important enough to assign an owner and a deadline.
CI/CD design can reduce the cost of flakiness
A lot of flaky test pain comes from how the pipeline is structured, not from the test itself. Continuous integration is supposed to reduce integration risk, but if the pipeline is overloaded with low-signal gates, it can become a bottleneck instead.
Separate fast feedback from deep verification
A common pattern is:
- PR checks, a small, stable, high-signal set
- post-merge checks, broader coverage with more tolerance for noise
- nightly validation, deeper tests and long-running scenarios
This helps keep the merge path clean while preserving broader coverage elsewhere.
Example GitHub Actions pattern
name: tests
on: [pull_request]
jobs: ui-smoke: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx playwright test –grep @smoke
The point is not the tool. The point is that the suite in the merge path should be small, deterministic, and worth blocking on. If the suite is unstable, you are teaching the organization to distrust CI.
Avoid hiding instability behind parallelism
Parallel test execution is good for speed, but it can also expose coupling bugs and environment contention. If adding parallelism increases flakes, you likely have shared-state or infrastructure problems that need fixing. Throwing more agents at the problem only makes it cheaper to ignore, not cheaper to solve.
How to measure flaky test cost without inventing fake precision
Teams often want a single number for flaky test cost. That is understandable, but there is no universal formula that works for every environment. Still, you can measure enough to make better decisions.
Track a few practical metrics:
- failure rate per test over time
- retry frequency
- quarantine count and age
- mean time to triage
- mean time to fix
- percentage of pipeline failures caused by known flakes
- number of release delays attributable to automation noise
These metrics will not tell you the exact dollar cost, but they will tell you where the cost is concentrated.
A useful rough model is:
flaky test cost = triage time + rerun time + delay time + trust decay + maintenance effort
Not every term is easy to quantify, but even a qualitative breakdown forces more honest prioritization.
A policy I would actually use
If I were defining a team policy from scratch, I would use something like this:
- A flaky failure is logged with enough context to reproduce or classify it.
- The first retry is allowed only for known transient failure classes.
- Any test that needs more than one retry is flagged for review.
- Quarantine is temporary, owned, and reviewed weekly.
- Every quarantined test has an exit criterion and a target removal date.
- Critical-path tests are kept small, stable, and narrowly scoped.
- Repeated flakes in the same area are treated as a product or environment reliability issue, not a test-only issue.
- Release decisions are based on a documented risk policy, not on whoever is on call that day.
This policy does not eliminate flakes. It makes them expensive enough to notice and structured enough to fix.
Common mistakes that make flaky tests more expensive
Treating all flakes as equal
A one-off network blip and a deterministic selector bug are not the same thing. If you handle them the same way, you waste triage time and dilute accountability.
Overusing retries
Retries are acceptable as a boundary defense, but not as a substitute for reliability work. If you need retries to feel safe, your gate is not actually safe.
Leaving quarantined tests unmanaged
The longer a quarantine lives, the more normalized the underlying issue becomes.
Letting product teams ignore automation failures
A flaky test in a critical workflow may be caused by app code, infrastructure, data, or automation. If the ownership model makes it “someone else’s problem,” the issue will linger.
Optimizing for green dashboards instead of trustworthy ones
A green pipeline is valuable only if the green means something. If the team learns to ignore failures, the dashboard is decorative.
The real goal is trustworthy signal at an acceptable cost
Flaky tests are not just a quality nuisance, they are a decision-making problem. The point is not to make the suite perfectly stable at any price. The point is to keep the testing system cheap enough to maintain and trustworthy enough to protect releases.
That usually means a mix of tactics, not a single fix:
- fix the unstable test where possible
- add limited retries for known transient failures
- quarantine only with ownership and deadlines
- keep critical gates small and high value
- improve data isolation and environment stability
- review flake trends as part of engineering health
If you do those things well, flaky test cost goes down for the right reasons. More importantly, release risk stays visible instead of being quietly hidden behind a green checkmark.
For teams building serious automation, that is the standard worth aiming for.