Why Are My End-to-End Tests Flaky, and How Do I Fix Them?
## The short answer
End-to-end tests flake — pass and fail without code changes — almost always because of timing issues, unreliable test data, shared state between tests, or dependence on unstable external services. The fix is rarely "add a longer wait". It's to make your tests deterministic: wait for real conditions rather than fixed delays, isolate each test's data, and control or stub the things you don't own. Flakiness is a defect in the test, not a quirk to tolerate.
A flaky suite is worse than no suite, because teams quickly learn to ignore red builds.
## Why flakiness is so damaging
When tests fail randomly, developers stop trusting them. They re-run the build until it goes green, which means a genuine regression can slip through disguised as "just a flake". The whole point of E2E tests — confidence — evaporates. Treating flakiness as a first-class bug, with the same urgency as a production defect, is the only sustainable posture.
## Root cause 1: timing and race conditions
The most common culprit. The test clicks a button and immediately checks for a result, but the application hasn't finished rendering or the network call hasn't returned.
Fixes:
- **Replace fixed sleeps with explicit waits.** Wait for an element to be visible, for text to appear, or for a network request to complete — not for an arbitrary number of seconds.
- **Use your test framework's auto-waiting features.** Modern tools retry assertions until they pass or time out, which removes most manual waits.
- **Wait for the right signal.** Waiting for a spinner to disappear is more reliable than waiting for a guessed duration.
## Root cause 2: test data and ordering
Tests that share data interfere with each other. If test A creates a record that test B assumes is absent, the result depends on run order.
Fixes:
- **Make each test create its own data** and clean up after itself, or use unique identifiers (timestamps, random suffixes) so tests never collide.
- **Never depend on test execution order.** Each test should pass in isolation.
- **Reset state between runs.** Seed a known baseline, or use transactions that roll back.
## Root cause 3: shared and leaked state
Global state — a logged-in session, a feature flag, a cached value — can leak from one test into the next.
Fixes:
- **Start each test from a known, clean state.** Fresh browser context, fresh session.
- **Avoid relying on state set up by a previous test**, even when it's convenient.
- **Isolate parallel workers** so concurrent tests don't write to the same data.
## Root cause 4: unstable external dependencies
If your test hits a real third-party API, payment sandbox, or email provider, their downtime becomes your flakiness.
Fixes:
- **Stub or mock external services** at the boundary for most tests, so you're testing your code, not theirs.
- **Keep a small number of true end-to-end smoke tests** against real services if you genuinely need that coverage, and accept they'll occasionally fail.
- **Add sensible retries only at the integration boundary**, never to mask logic bugs.
## Root cause 5: the environment itself
Underpowered CI runners, network contention, and resource limits cause timeouts that look like flakes.
Fixes:
- **Give CI enough resources** to run the browser smoothly.
- **Run tests in parallel deliberately**, with proper isolation, rather than accidentally.
- **Monitor test duration** — a test that's creeping towards its timeout is a flake waiting to happen.
## How to find and fix flakes systematically
1. **Quarantine, don't delete.** Move a flaky test out of the blocking suite so it stops poisoning the build, then fix it promptly.
2. **Track flake rates.** Record how often each test fails on retry. The worst offenders reveal themselves.
3. **Reproduce locally** by running the test many times in a loop. Flakes that only appear under repetition are the timing kind.
4. **Fix the root cause, not the symptom.** Adding a retry around a racy assertion hides the problem; fixing the wait condition solves it.
At neart.ai, building enterprise-grade products means our E2E suites have to be trusted gates, not noise — which is why we treat every flake as a defect to be eliminated rather than re-run away.
## Takeaway
Flakiness is not random; it's caused by timing, data, shared state, and unstable dependencies — all fixable. Replace fixed waits with condition-based waits, isolate each test's data and state, stub what you don't control, and treat every flaky test as a bug to fix or quarantine. A deterministic suite is the only kind worth keeping green.