# Why Playwright Tests Flake: 7 Root Causes and Fixes

> A Playwright test that passes on retry is still a failed reliability signal. Use this diagnostic workflow to trace flaky tests to locators, timing, shared state, network behavior, and CI pressure—and fix the cause instead of hiding it.

- Author: [Shashank Rawlani](https://shashank.rawlani.com)

- Published: 2026-08-11T17:58:45.986Z

- Updated: 2026-08-11T17:58:45.997Z

- Category: Automation Tutorials

- Canonical source: https://automationtester.in/blog/automation-tutorials/why-playwright-tests-flake-root-causes-fixes

- Tags: playwright, flaky-tests, test-automation, ci-cd, reliability

**A Playwright test that passes on retry is not green. It is a failed reliability signal wearing a green badge.**

Playwright makes browser automation more reliable through locator retryability, actionability checks, web-first assertions, isolated browser contexts, traces, and worker isolation. None of those features can make an ambiguous requirement, shared database row, unstable third-party API, or overloaded CI runner deterministic.

The useful question is not “How many retries should we add?” It is “Which boundary changed between the passing and failing run?” This guide gives you a repeatable way to answer that question.

**Start here:** If you have raw failure exports, use [FlakeRadar](https://automationtester.in/tools/flake-radar) to group repeat failures. Then use the [Playwright Locator Advisor](https://automationtester.in/tools/playwright-locator) to inspect brittle selectors and the [API Response Time Analyzer](https://automationtester.in/tools/api-response-time) to separate UI timing from backend latency.

## What Playwright actually calls flaky

When retries are enabled, Playwright classifies a test as *flaky* when its first run fails and a retry passes. That classification is valuable evidence. It tells you the suite observed at least two outcomes for what should have been the same test contract.

Retries are useful for collecting a second trace or keeping a larger pipeline moving while a failure is investigated. They are not the fix. If the only change is a retry count, the nondeterminism remains and can still conceal a product defect.

## A seven-part root-cause model

### 1. The locator describes the DOM, not the user contract

Long CSS chains, XPath tied to layout, and positional selectors such as `nth(2)` often encode an implementation accident. A harmless wrapper, a reordered list, or a responsive variation changes the DOM while the user-facing control remains the same.

```
// Brittle: structure and position are part of the selector.
await page.locator('#checkout > div:nth-child(2) button').click();

// Resilient: role and accessible name describe the user contract.
await page.getByRole('button', { name: 'Place order' }).click();
```

Prefer role, label, text, and explicit test-ID contracts. Use the locator advisor to compare options, and validate unavoidable CSS or XPath with the [CSS Selector Tester](https://automationtester.in/tools/css-selector) or [XPath Tester](https://automationtester.in/tools/xpath-tester).

### 2. Auto-waiting is mistaken for business-state waiting

Before an action, Playwright waits for conditions such as visibility, stability, event reception, and enabled state. It does not know that a background import finished, a ledger entry reached the database, or an eventual-consistency window closed.

```
await page.getByRole('button', { name: 'Import' }).click();

// Assert the user-visible business result instead of sleeping.
await expect(page.getByRole('status')).toHaveText('Import complete');
await expect(page.getByRole('row', { name: /customer-1042/ })).toBeVisible();
```

A hard wait only says “enough time passed on this machine.” A web-first assertion says “the state required by the test is now true.”

### 3. Tests share mutable state

Parallel workers expose collisions that sequential local runs hide. Two tests update the same user, reuse the same order number, empty the same queue, or rely on suite order. The second test becomes dependent on timing instead of its fixture.

```
import { test as base } from '@playwright/test';

export const test = base.extend({
  accountName: [async ({}, use, workerInfo) => {
    const name = 'e2e-worker-' + workerInfo.workerIndex;
    await provisionAccount(name);
    await use(name);
    await deleteAccount(name);
  }, { scope: 'worker' }],
});
```

Use unique data per test or worker, create state through APIs or fixtures, and make cleanup idempotent. Browser-context isolation does not isolate your database.

### 4. The test depends on an uncontrolled network boundary

A live analytics endpoint, payment sandbox, feature-flag service, or rate-limited API can fail independently of the product behavior under test. Decide whether the boundary is part of the contract.

- If the boundary is not under test, route and fulfill it with a deterministic response.

- If integration is the purpose, assert the status, response schema, timeout budget, and failure behavior explicitly.

- If latency is suspected, measure it rather than increasing the page timeout globally.

Use the response-time analyzer for captured requests and [JSON Diff](https://automationtester.in/tools/json-diff) when pass and fail runs return structurally different payloads.

### 5. A timeout is used as a synchronization strategy

`waitForTimeout(3000)` makes every fast run slower and every run slower than three seconds fail. It also removes the most useful diagnostic: which condition never became true.

Replace sleeps with a condition at the correct layer: locator assertion for visible state, response wait for a specific request, or an API/database poll for a backend transition. Keep timeouts local to the slow operation instead of inflating the entire suite.

### 6. Motion, overlays, and virtualized UI change the action target

A cookie banner intercepts a click, an animation moves a button, or a virtualized row is recycled between lookup and action. Playwright's actionability checks catch many of these cases, but the right fix is still to model the UI state.

- Close or seed consent state in setup.

- Wait for the stable, user-visible state rather than forcing the click.

- Scope a locator to a stable container and assert uniqueness before acting.

- For virtualized lists, search or scroll by a meaningful item identity.

`force: true` is a last resort because it bypasses part of the actionability contract and can make the test behave unlike a user.

### 7. CI has a different resource and environment profile

CI may have fewer cores, slower storage, different fonts, a cold cache, proxy latency, or more parallel consumers hitting the same environment. Increasing workers beyond available cores can create the timeouts you are trying to remove.

Pin the Playwright image and browser version, record traces on first retry, match CI configuration locally when possible, and measure worker-level duration. If failures begin only after sharding, inspect shared data and environment capacity before blaming the browser.

## A diagnostic workflow that produces evidence

- **Make the failure observable.** Retain a trace on the first retry and keep screenshots or video only when they answer a specific question.

- **Measure recurrence.** Run the smallest failing scope with `--repeat-each`. Record the exact command, browser, worker count, and environment.

- **Compare the last good and first bad boundary.** In the trace, check the locator result, actionability log, console error, network response, and DOM snapshot in that order.

- **Classify the cause.** Put it in one of the seven buckets above. “Timing” is not a root cause until you name the state that raced.

- **Fix the owning layer.** A locator problem belongs in the selector contract; a data collision belongs in fixtures; an API latency regression belongs in the service or its threshold.

- **Prove the fix under stress.** Repeat the focused test, then run it with the same workers and sharding used in CI.

- **Keep flakiness visible.** Configure CI to fail on detected flaky tests where supported, or report flaky classifications as a quality gate.

## A practical Playwright configuration

```
import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  failOnFlakyTests: Boolean(process.env.CI),
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
});
```

One retry is enough to classify and capture evidence. It should not become permission to leave the failure unresolved. Tune worker count from measured runner capacity, not from the number that finishes fastest on a laptop.

## What to do on the next flaky failure

Do not start by adding a sleep. Open the trace, identify the last correct boundary, and assign the failure to one of seven causes. If the locator is ambiguous, fix the user-facing contract. If state is shared, isolate it. If the service is slow, measure and gate it. If CI alone fails, reproduce its resource and concurrency profile.

The immediate action is simple: take one test currently protected by retries, run it repeatedly without that safety net, and document the real condition it is waiting for. That converts “flaky” from a label into an engineering task.

## Primary references

- [Playwright retries and flaky classification](https://playwright.dev/docs/test-retries)

- [Playwright locator guidance and strictness](https://playwright.dev/docs/locators)

- [Playwright auto-waiting and actionability](https://playwright.dev/docs/actionability)

- [Playwright Trace Viewer](https://playwright.dev/docs/trace-viewer)

- [Playwright worker and data isolation](https://playwright.dev/docs/test-parallel)

Source: [Why Playwright Tests Flake: 7 Root Causes and Fixes](https://automationtester.in/blog/automation-tutorials/why-playwright-tests-flake-root-causes-fixes) by Shashank Rawlani.
