Playwright flakiness

How to fix flaky tests in Playwright.

Playwright is more reliable than Selenium, but flaky tests still happen. Here is exactly what to check and how to fix each category.

Why Playwright tests still flake

Playwright's auto-waiting is a significant improvement over raw WebDriver, but it does not eliminate flakiness. Tests still fail due to timing assumptions, brittle selectors, shared state, and environment differences. The difference is that when Playwright tests flake, the root cause is usually a clear pattern rather than a generic race condition.

Understanding these patterns is the first step to eliminating them. For context on how flakiness affects overall release confidence, see how to measure release confidence like an engineering leader.

Key insight: Playwright's auto-waiting handles common timing issues, but it cannot fix tests that make incorrect assumptions about network state, animation completion, or third-party content loading. These require explicit wait strategies.

Timing anti-patterns and auto-waiting

The most common cause of flaky Playwright tests is relying on implicit timing rather than explicit conditions. Playwright auto-waits for element visibility and actionability, but it does not auto-wait for network responses or data mutations.

Anti-patternWhy it flakesFix
page.waitForTimeout(n)Fixed delay fails under varying network speedUse waitForResponse or waitForSelector
Asserting immediately after navigationPage may not be fully renderedAdd waitForLoadState('networkidle')
No wait after form submissionAPI response may not complete before assertionUse waitForResponse matching the API call
Assuming animation is instantCSS transitions can delay element availabilityWait for final state, not intermediate transitions
Warning: page.waitForTimeout is the most dangerous API in Playwright for flakiness. It creates a false sense of stability. Tests pass locally and fail in CI because local and CI network speeds differ. Remove all fixed-timeout waits and replace them with condition-based waits.

Selector strategy for long-term stability

Selector fragility is the second most common cause of flaky Playwright tests. The fix is a clear selector hierarchy:

  1. Prefer getByRole for accessibility-based selection. These selectors are stable across design iterations and align with accessibility requirements.
  2. Use getByText for visible labels. Great for buttons, links, and headings where the text is unlikely to change.
  3. Add data-testid attributes for critical interactions. This is the most stable option for elements that need to be targeted across many tests.
  4. Avoid CSS class selectors. CSS classes change frequently during refactoring and are the number one source of selector breakage.

For a comprehensive approach to building automation suites that stay stable over time, see QA automation architecture services.

State isolation and test fixtures

Tests that share state — whether through browser context, storage, or backend data — will fail intermittently. Playwright provides excellent tools for isolation:

  • Use test.beforeEach to set up fresh state. Never reuse state from a previous test even if the test order seems predictable.
  • Create isolated test fixtures. Each test should create its own user, session, and data set rather than depending on shared setup.
  • Use context.clearCookies() and context.clearPermissions() between tests if tests must share a browser context.
  • Prefer test.describe.configure({ mode: 'parallel' }) by default. Serial execution hides state dependencies that will surface in parallel mode.

This isolation approach mirrors the patterns used in the Flipkart case study, where zero state leakage was required to maintain 99 percent test stability at scale.

CI-specific failure patterns

Tests that pass locally but fail in CI are almost always environment-related. Common CI-specific issues include:

  • Headless rendering differences: Font loading, image rendering, and layout can differ between headless and headed modes. Use --headed in CI for debugging.
  • CPU throttling: CI runners have limited CPU. Animations and transitions that complete locally may not finish in CI. Disable CSS animations in test configurations.
  • Screen resolution: Default CI resolutions differ from developer machines. Set a consistent viewport size with page.setViewportSize().
  • Network latency: CI environments may have different caching and latency characteristics. Use Playwright's page.route() to mock network responses for critical API calls.

The Oracle release confidence case study documents how standardizing CI environment configuration reduced environment-related flakiness by 60 percent in the first month.

The Playwright flakiness triage workflow

Test fails in CI
↓
Passes locally?
Yes → CI environment issue No → Consistent failure?
↓
Yes → Real defect No → Flaky test
↓ Root cause
Timing → Add explicit waits Selectors → Use data-testid State → Isolate tests

Use this triage workflow every time you encounter a test that behaves unpredictably. It ensures you don't waste time debugging real defects as flaky or vice versa.

Before-and-after code patterns

Replace waitForTimeout with network-aware waits

// Before: brittle fixed timeout
await page.waitForTimeout(5000);
await expect(page.locator('.results')).toBeVisible();

// After: wait for the API response
const responsePromise = page.waitForResponse('**/api/search');
await page.getByRole('button', { name: 'Search' }).click();
await responsePromise;
await expect(page.getByTestId('results')).toBeVisible();

Use test fixtures for isolated state

// Before: shared setup creates hidden dependencies
let user;
test.beforeAll(async () => {
  user = await createUserInDB();
});

// After: isolated fixture per test
test('can update profile', async ({ page }) => {
  const user = await createUserInDB({ unique: true });
  await page.goto(`/profile/${user.id}`);
  // No risk of state leakage from other tests
});

Mock network for deterministic tests

// Before: depends on real API availability
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();

// After: mock the API for deterministic behavior
await page.route('**/api/user', async route => {
  await route.fulfill({ json: { name: 'Test User' } });
});
await page.goto('/dashboard');
await expect(page.getByText('Welcome, Test User')).toBeVisible();

Prevention and health checks

Once your Playwright tests are stable, prevent regression with these practices:

  • Run with --repeat-each 5 on critical tests to surface intermittent failures before they reach production CI.
  • Capture traces on failure using --trace on so every flaky failure has a replayable trace.
  • Set a flaky rate budget. If the Playwright suite exceeds 3% flaky rate, pause new test development until stability is restored.
  • Review selectors in every PR. Require that new tests use getByRole or getByTestId rather than CSS selectors.
Real-world benchmark: Teams that implement these practices consistently maintain Playwright flaky rates below 1%. The Logitech case study shows how a multi-framework program achieved this standard across 6 locales.

For the broader context on suite health, read how to fix flaky test suites and CI quality gates that improve release confidence. You can also review the Release Confidence Sprint if you need a structured engagement to fix this fast.

Get the Playwright flakiness checklist

A one-page PDF checklist covering every common flakiness pattern in Playwright with before-and-after code examples. No email required.

Get the checklist via email
Rahul Kant Jha

Rahul Kant Jha

Test Automation & Release Confidence Consultant and Test Automation & Release Confidence Consultant. I help engineering teams fix broken test automation, reduce regression cycle time, and build CI signal that leadership trusts. Book a free diagnosis call or see fixed-price pricing.

Related Insights

Email RahulBook diagnosis

Need help implementing this?

I fix this exact problem for engineering teams — typically in 20 days or less.