Flaky tests

How to fix flaky tests in Playwright without hiding them

AI Testing Platform · 26 Sept 2026 · 4 min read

Most flaky Playwright tests are fixed by removing fixed timeouts, finding elements through locators that describe what the user sees, and giving each test its own state. Playwright already performs actionability checks and auto-waits for them before actions [Microsoft (Playwright)], so a test that still flakes usually waits on the wrong thing or shares state with another test. Retries should report flakiness, not hide it.

Fixed waits are the first thing to remove

A fixed wait pauses the test for a set time and hopes the page is ready. On a fast laptop the time is too long. On a busy continuous integration (CI) runner it is too short. Either way the test is guessing.

Playwright checks that an element is ready before acting on it. It auto-waits for all the relevant checks to pass, and fails the action with a TimeoutError if the checks do not pass within the given timeout [Microsoft (Playwright)]. A fixed wait before an action adds nothing to that, except run time and a new way to fail.

ts
1// Fragile: waits a fixed time, then clicks by CSS id2await page.waitForTimeout(3000);3await page.click('#submit');45// Sturdier: finds the button by role and name; the click waits for it6await page.getByRole('button', { name: 'Submit' }).click();

This matches research on flaky tests in general. Async Wait is one of the top three categories of flaky tests, next to Concurrency and Test Order Dependency [University of Illinois at Urbana-Champaign / ACM FSE], and many Async Wait flaky tests (54%) are fixed using waitFor [University of Illinois at Urbana-Champaign / ACM FSE]. Waiting on a condition beats waiting on a clock.

Locators should describe what the user sees

Locators are the central piece of Playwright's auto-waiting and retry-ability [Microsoft (Playwright)]. How you write them decides how often tests break for reasons unrelated to the product.

A selector tied to a CSS class or a deep element path breaks when a developer restyles the page. A locator based on role and visible name breaks only when the thing the user sees changes. Prefer role and label over class names and element paths. When no role fits, add a dedicated test attribute rather than relying on styling.

Check the final state the user sees through a locator too. Reading a value once and comparing it later skips the waiting that locators provide.

ts
1await expect(page.getByRole('heading', { name: 'Order placed' })).toBeVisible();

A test that fails because a button was renamed is not flaky. It fails every time until updated. That case is covered in what causes flaky tests.

Tests must not share state

If a test passes alone and fails in the full run, look for shared state. Test Order Dependency and Concurrency are the other two of the top three categories [University of Illinois at Urbana-Champaign / ACM FSE]. Both mean one test depends on something another test did.

Practical rules:

  • Each test creates the records it needs, with unique names.
  • Each test starts from a clean browser context, with its own cookies and storage.
  • Log in through a setup step per test or per worker, not by reusing a session another test left behind.
  • Tests that must run in sequence are marked that way on purpose, with a comment saying why.

Parallel runs make these problems visible sooner. That is useful. A suite that only passes in serial is hiding coupling.

Retries are a detector, not a fix

Retries are a common answer to flaky Playwright tests, and an easy one to misuse. A retry that turns a red run green removes the signal without removing the cause.

Use retries in CI, but treat every test that passed only on retry as flaky and record it. Review that list each week. Fix the tests at the top, and quarantine the ones you cannot fix yet, with an owner and a date. Keep retries off locally so developers see failures as they happen. The full trade-off is in retry or quarantine flaky tests, and collecting the signal is covered in detect flaky tests in CI.

Run on the browsers you ship to

Playwright can run tests on Chromium, WebKit and Firefox browsers as well as branded browsers such as Google Chrome and Microsoft Edge [Microsoft (Playwright)]. A test that passes in one engine and fails in another is often reported as flaky when it is a real browser difference.

Run the browsers your users use, and keep results separate per browser. A failure that appears in one engine only is a finding, not noise.

How AI Testing Platform relates to Playwright

AI Testing Platform takes a different route to the same problems: tests are plain-English steps in a YAML file, resolved against the live page at run time, with locator and layout changes healed and recorded for review. Each failed test gets a triage card with a proposed classification of app bug, test issue or environment. A side-by-side view is on compare with Playwright, and CI setup is in the CI docs.

Sources

  1. Microsoft (Playwright), Auto-waiting | Playwright. Checked 2026-09-26.
  2. Microsoft (Playwright), Locators | Playwright. Checked 2026-09-26.
  3. Microsoft (Playwright), Browsers | Playwright. Checked 2026-09-26.
  4. University of Illinois at Urbana-Champaign / ACM FSE, An Empirical Analysis of Flaky Tests (Luo, Hariri, Eloussi, Marinov, FSE 2014). Checked 2026-09-26.
  5. University of Illinois at Urbana-Champaign / ACM FSE, An Empirical Analysis of Flaky Tests (Luo, Hariri, Eloussi, Marinov, FSE 2014). Checked 2026-09-26.

Questions

Should I use retries for flaky Playwright tests?

Use retries in continuous integration (CI) to detect flaky tests, and record every test that passed only on retry. Fix the tests at the top of that list and keep retries off locally so failures stay visible.

Why does my Playwright test pass locally but fail in CI?

The usual reasons are fixed waits that are too short on a busy runner, shared state between tests that run in parallel, or a different browser engine. Replace fixed waits with conditions and give each test its own data.

Is waitForTimeout bad in Playwright?

A fixed timeout guesses how long the page needs. It makes tests slower on fast machines and flaky on slow ones. Waiting for a visible condition through a locator is more reliable.

Request early access

Early access: prices are the planned list prices. Billing starts when the platform enters beta; early-access use is not charged. We reply within 24 hours on business days.