1. Home
  2. Blog
  3. Test Automation

Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

A test that fails at random teaches your team to ignore failures. Learn how to confirm a test is flaky, track down the real cause, and fix it, with demos showing what actually works.

9 min read
Flaky tests: what causes them and how to fix them for good

Key takeaways

  • A flaky test passes and fails on the same code, with no change.
  • The usual causes are timing, shared state, unstable data, external services and environment differences.
  • Replace fixed sleeps with waits for a specific condition, and give every test its own data.
  • Retries can hide real bugs. Quarantine a flaky test, fix it within a deadline, or delete it.

The build goes red, someone clicks “re-run”, it passes, and everybody moves on. It seems harmless, but flaky tests slowly destroy the most valuable thing a test suite has: people’s trust in it. When failures are usually noise, real bugs get waved through too.

This guide covers why tests become flaky and how to fix each cause properly. We ran the timing and shared-state demos in Python 3.13, and the numbers shown come from those runs. They are small demonstrations designed to make flakiness easy to see, so your own failure rates will differ.

What is a flaky test?

A flaky test gives different results on the same code. It passes on one run and fails on the next, even though nothing in the product changed. The test may be at fault, or the code may contain a real intermittent bug. That second possibility is exactly why you should not simply dismiss them.

Why flaky tests are so costly

  • Lost trust. Once people assume red means “probably flaky”, they stop investigating real failures.
  • Wasted time. Developers spend hours rerunning pipelines and examining failures that have nothing to do with their change.
  • Slower delivery. Merges and releases wait on reruns, undermining the point of CI/CD.
  • Hidden bugs. Race conditions in the product can look just like test noise, and those are the ones that hurt in production.

Common causes at a glance

Cause Typical sign Usual fix
Timing and async Fails more when CI is busy Wait for a condition
Shared state Fails depending on test order Isolate data, reset state
Dates and randomness Fails at certain times or days Control the clock and seed
Brittle selectors Fails after UI changes Use stable locators
External services Fails when a dependency is slow Mock or stub them
Environment differences Passes locally, fails in CI Align with containers
Resource limits Fails under parallel runs Tune workers, fix contention

Cause 1: Timing and fixed sleeps

This is the most common cause. A test assumes that background work will be finished after a fixed delay. To see the effect, we wrote a small test in which the work takes up to 2 ms, while the test waits for just 1 ms before checking:

def flaky_timing():
    result = {}
    def work():
        time.sleep(random.uniform(0, 0.002))
        result["done"] = True
    threading.Thread(target=work).start()
    time.sleep(0.001)        # hope it has finished
    assert result.get("done")

The fix is to wait for the actual condition, with a generous upper limit:

def fixed_timing():
    result = threading.Event()
    def work():
        time.sleep(random.uniform(0, 0.002))
        result.set()
    threading.Thread(target=work).start()
    assert result.wait(timeout=1)   # continues as soon as it is ready

We ran each version 200 times:

Version Failures out of 200
Fixed sleep 102
Wait for a condition 0

The result is more than just reliability. The fixed version moves on the moment the work is done, so it is usually faster too. A longer sleep would make the first version fail less often, but every run would then be slower, and it could still fail on a busy CI machine.

In browser tests, the same principle applies. Use the auto-waiting of Playwright, or explicit Selenium waits, instead of time.sleep:

# Playwright: retries until true or a timeout is reached
expect(page.get_by_text("Order placed")).to_be_visible()

# Selenium: an explicit wait for a condition
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "confirmation"))
)

Cause 2: Shared state and test order

When tests share data, one can affect another, and the result depends on the order they run in. These two tests use one shared list:

store = []

def test_add():
    store.append("x")
    assert len(store) == 1

def test_empty():
    assert store == []
Order test_add test_empty
empty, then add Pass Pass
add, then empty Pass Fail

Our run confirmed this, and it is why a suite can pass for months and then break when someone adds parallel runs or a random ordering plugin. The cure is isolation:

  • Give each test fresh data, using a fixture that sets it up and cleans it up. Our pytest tutorial shows how.
  • Create unique records, such as an email with a random suffix, in place of a shared “test user”.
  • Wrap database tests in a transaction and roll it back, where your setup allows it.
  • Run tests in random order regularly, so that these problems show up early.

Cause 3: Dates, time zones and randomness

A test that passes all week can fail on a Saturday, at midnight or on the last day of the month. Make time an input, rather than reading the clock inside your logic:

def is_weekend(today=None):
    today = today or datetime.date.today()
    return today.weekday() >= 5

is_weekend(datetime.date(2026, 11, 7))   # True  (a Saturday)
is_weekend(datetime.date(2026, 11, 9))   # False (a Monday)

Those tests give the same answer on any day. Likewise, set a fixed seed for random values in tests, and log it when you use random data so that you can reproduce a failure. Do not assume a particular order from sets, dictionaries from external sources or database queries with no ORDER BY.

Cause 4: Brittle selectors

A test that finds a button with div:nth-child(3) > span breaks with any layout tweak, and it may fail only for some screen sizes or variants. Prefer locators based on what users see, or dedicated test attributes. Our guide to Playwright locators explains the order to try them in. Animations are another frequent source of trouble, so consider disabling them in test environments.

Cause 5: External services and networks

If a test calls a real payment provider or a third-party API, then its result depends on someone else’s uptime and rate limits. Mock those calls for most tests, and keep a small number of separate contract checks against the real service, so that you still learn if it changes. Our guide to unit vs integration testing explains how to split these up.

Tip: “Passes locally, fails in CI” usually means a difference in speed, time zone, resources or versions. Running tests in the same container image locally and in CI removes many of these. Our Docker Compose tutorial can help.

How to diagnose a flaky test

  1. Confirm it. Run the test many times without changes. With pytest, a simple shell loop works, and a plugin such as pytest-repeat gives you --count=100.
  2. Record evidence. Save logs, screenshots and traces for each failure. Playwright’s trace viewer is particularly useful here.
  3. Stress the conditions. Run it with limited CPU, in parallel or in random order. Making it fail more often makes it easier to study.
  4. Test it alone. If it always passes alone but fails in the suite, suspect shared state.
  5. Look for patterns. Check which machines, times of day and neighbouring tests are involved.
  6. Check the product. Ask honestly whether this could be a real race condition in your application.

Are automatic retries a fix?

Retries are a useful short-term safety net, and they are not a cure. A single automatic retry in CI can keep work moving while you investigate, but a test that needs a retry to pass is still telling you something. Worse, a real intermittent product bug that fails 20% of the time will usually get through a retry unnoticed.

Use retries with these rules: limit them to one or two, record every test that needed one, and treat that list as your to-do list. A test reported as “passed on retry” should count as a problem to investigate, not a success.

A team process for flaky tests

  1. Measure. Track which tests fail and then pass on the same commit. Many CI tools and test reporters can show this.
  2. Quarantine. Move a confirmed flaky test out of the blocking run, so that it stops disrupting everybody, but keep running it and tracking it.
  3. Assign an owner and a deadline. A quarantine with no deadline just becomes a graveyard.
  4. Fix or delete. If a test is not worth fixing, remove it, and check that its coverage still exists elsewhere.
  5. Prevent. Run new tests several times before merging. A test that fails once in fifty runs is better caught now than in six months.

Treat a flaky test as a bug with priority, and not as background noise. Severity and priority apply here as well, as covered in severity vs priority. If flakiness is widespread, read about broader test automation mistakes, and consider whether you have too many tests at the browser level.

Final thoughts

Flaky tests feel random, but they rarely are. Nearly every one comes down to an assumption about time, order, data or environment that is not always true. Pick the test that fails most often in your pipeline this week. Run it fifty times, find the assumption, and fix that for good. One real fix teaches a team more than a hundred reruns ever will.

Frequently asked questions

What is a flaky test?

A flaky test passes on some runs and fails on others without any change in the code. The cause may be in the test, the environment or the product.

What causes flaky tests?

The most common causes are timing assumptions, shared state between tests, unstable selectors, external services, date handling and differences between CI and local machines.

Should I delete flaky tests?

First try to fix them. If a test is low value and costly to fix, deleting it is fine, but make sure its important coverage exists somewhere else.

Should I retry failed tests automatically?

As a limited safety net, yes, but record each retry and investigate it. Retries alone hide problems and can let real intermittent bugs reach production.

How do I find flaky tests?

Look for tests that fail and pass on the same commit, using your CI history or a reporting tool. You can also run suspect tests many times in a loop.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Software Development

Python Dataclasses Explained: Less Boilerplate, Safer Defaults and Real Examples

Python dataclasses explained with code we ran: generated methods, default_factory, frozen and ordered classes, __post_init__, slots, kw_only and when to…

Read article →
Software Development

JavaScript Destructuring Explained: Objects, Arrays, Defaults and Common Errors

JavaScript destructuring explained with code we ran in Node.js: objects, arrays, renaming, defaults, nested patterns, rest, function parameters and the…

Read article →
Software Development

Python f-strings Explained: Formatting Numbers, Dates and Debug Output

Python f-strings explained with code we ran: format specifiers for numbers, alignment and dates, the = debug shortcut, rounding surprises,…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
DevOps

Environment Variables Explained: .env Files, Secrets and Config Done Right

Environment variables explained with Python examples we ran: reading and setting them, why every value is a string, .env files,…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Databases & SQL

DELETE vs TRUNCATE vs DROP in SQL: Differences, Rollback and When to Use Each

DELETE vs TRUNCATE vs DROP explained with tested SQL: what each removes, rollback, triggers, identity reset, speed, and how PostgreSQL,…

Read article →
Databases & SQL

SQL Views Explained: Create, Use and Avoid the Pitfalls, With Examples

SQL views explained with queries we ran: creating a view, live results, hiding columns, performance, updatable and materialized views, broken…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
DevOps

Monitoring vs Observability: The Real Difference, With Logs, Metrics and Traces

Monitoring vs observability explained: known and unknown failures, logs, metrics and traces, a checkout incident walkthrough, golden signals, OpenTelemetry and…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →
Software Testing

Test Case vs Test Scenario: Key Differences, Examples and When to Use Each

Test case vs test scenario explained: clear definitions, a side-by-side table, one password reset feature taken from scenarios to cases,…

Read article →
Artificial Intelligence

What Is a Large Language Model? How LLMs Work, Explained Without the Hype

What is a large language model? LLMs explained in plain language: tokens, embeddings, attention, training, temperature and context windows, a…

Read article →
Test Automation

Selenium vs Playwright: Key Differences, Code Compared and Which to Choose

Selenium vs Playwright compared: architecture, waiting, browser and language support, debugging, the same test in both tools, migration advice and…

Read article →