CI/CD Guide September 22, 2026 16 min read

How to Fix Flaky Playwright Tests in CI/CD (2026 Guide)

Flaky tests erode confidence, waste CI minutes, and slow down releases. This guide walks you through diagnosing the root causes of flaky Playwright tests and eliminating them systematically -- with code examples, configuration patterns, and an AI-powered debugging workflow.

⚠️

Teams waste 20-30% of CI time on flaky tests

This guide eliminates the root causes. Every fix is backed by real-world patterns from production Playwright suites running thousands of tests per day.

You merge a pull request. CI goes red. You check the logs, rerun the pipeline, and it passes. Sound familiar? Flaky tests are the single biggest productivity killer in test automation. They drain CI budgets, erode developer trust in the test suite, and -- worst of all -- train teams to ignore real failures because "it's probably just flaky."

Playwright is one of the most reliable browser automation frameworks available, but no framework is immune to flakiness when tests are written without proper isolation, timing, and state management. This guide covers the seven most common causes of flaky Playwright tests, how to diagnose them, and how to fix each one with concrete code examples.

If you are new to Playwright debugging in general, start with our how to debug Playwright tests guide for foundational techniques. This article builds on those fundamentals with a specific focus on flakiness in CI/CD environments.


What Are Flaky Tests?

A flaky test is a test that produces different results (pass or fail) on the same code without any changes. Run it ten times and it passes eight. Run it again and it fails twice. The test code has not changed. The application code has not changed. But the result is non-deterministic.

Flaky tests are particularly destructive because they create a boy-who-cried-wolf problem. When CI fails five times a week due to flaky tests, developers stop investigating failures. They hit "rerun" and move on. Then, when a real bug causes a test failure, it gets lost in the noise. The test suite exists to catch regressions -- flakiness defeats that entire purpose.

The financial cost is real too. A mid-size team running 500 tests in CI with a 5% flake rate will see roughly 25 false failures per run. If each failure triggers a 15-minute investigation or rerun cycle, that is over six hours of wasted engineering time per week. At scale, companies report spending 20-30% of their total CI compute budget on reruns caused by flaky tests.

The good news: flaky Playwright tests almost always have identifiable root causes. They are not random. They are deterministic problems that manifest non-deterministically because of timing, state, or environment differences. Once you know the patterns, you can fix them systematically.

7 Common Causes of Flaky Playwright Tests

After analyzing thousands of flaky test reports across production Playwright suites, these seven causes account for over 95% of all flakiness. Understanding each one is the first step to eliminating it.

1. Race Conditions

The most common cause. Your test clicks a button, but the event handler has not attached yet. Your test reads text from a page, but an API response has not rendered yet. The test assumes something is ready when it is still loading. Locally, your fast machine finishes rendering in 50ms and the test passes. In CI, the same render takes 200ms and the test fails because the assertion fires before the content appears.

2. Shared State Between Tests

Tests that modify the same database record, local storage, cookies, or file system resource will interfere with each other. Test A creates a user, Test B deletes all users, Test C tries to log in as the user from Test A -- but it no longer exists. The order tests execute in determines whether they pass. Change the order (or run in parallel) and everything breaks.

3. Hardcoded Waits

Using page.waitForTimeout(2000) is a gamble. Two seconds might be enough on your local machine, but in CI under load, the operation might take three seconds. Or it might take 200ms locally, making the test unnecessarily slow. Hardcoded waits are always either too long (slow) or too short (flaky). There is no correct value because execution time is variable.

4. Network Timing

Tests that depend on real API responses are at the mercy of network latency. An endpoint that responds in 100ms locally might take 2 seconds in CI if the test environment has higher network latency or if the API server is under load. Tests that assert on data from API responses without waiting for those responses to complete will fail intermittently.

5. Date/Time Dependencies

Tests that assert on "today's date" or "current time" can fail at midnight, at month boundaries, or when CI servers are in different timezones than your local machine. A test that checks "September 22" will fail on September 23. A test that asserts "2 minutes ago" will fail if the CI server's clock is slightly off or the test takes longer than expected to run.

6. Viewport Differences

Your local browser opens at 1920x1080. The CI headless browser defaults to 1280x720. A button that is visible on your screen is below the fold in CI and requires scrolling. An element that fits on one line at 1920px wraps to two lines at 1280px, changing its height and pushing other elements around. The test clicks at coordinates that hit different elements at different viewport sizes.

7. Browser Caching

If tests share browser contexts or pages, cached resources from one test can affect another. A test that expects a fresh API response might get a cached one instead. Service workers registered by one test can intercept requests in subsequent tests. Session storage from a login test can leak into a test that expects an unauthenticated state.

For a comprehensive look at Playwright patterns that prevent these issues from the start, see our Playwright best practices for 2026 guide.

How to Diagnose Flaky Tests

Before you can fix a flaky test, you need to confirm it is flaky and understand its failure pattern. Playwright provides several built-in tools for this.

Use --repeat-each to reproduce

The fastest way to confirm flakiness is to run the suspect test multiple times:

Terminal -- reproduce flaky tests
# Run each test 20 times to surface intermittent failures
npx playwright test tests/checkout.spec.ts --repeat-each=20

# Run with full tracing to capture state on failure
npx playwright test tests/checkout.spec.ts --repeat-each=20 \
  --trace=retain-on-failure

# Run in serial mode to isolate from parallel interference
npx playwright test tests/checkout.spec.ts --repeat-each=20 \
  --workers=1

If the test fails on 2 out of 20 runs, you have confirmed flakiness. If it fails on all 20, you have a consistent bug -- not flakiness. If it passes all 20, try running it with --workers=4 to check if parallel execution triggers the failure, which would point to shared state.

Use the Trace Viewer

Playwright's trace viewer is the most powerful diagnostic tool for flaky tests. When you run with --trace=retain-on-failure, Playwright records a complete timeline of every action, network request, DOM snapshot, and console message for failing runs. Open the trace with:

Terminal -- open trace viewer
npx playwright show-trace test-results/checkout-test/trace.zip

In the trace viewer, look for: actions that took significantly longer than expected (timing issues), network requests that returned errors or unexpected data (API flakiness), DOM snapshots where the expected element was not yet visible (race conditions), and console errors that indicate JavaScript exceptions on the page.

Capture Screenshots and Videos on Failure

Add these settings to your playwright.config.ts to automatically capture diagnostic artifacts whenever a test fails:

playwright.config.ts -- diagnostic artifacts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Capture trace on first retry (flaky test diagnosis)
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  // Enable retries so Playwright marks flaky tests
  retries: process.env.CI ? 2 : 0,

  // Output directory for artifacts
  outputDir: 'test-results/',
});

With this configuration, your CI pipeline will produce trace files, screenshots, and videos for every failed test. Upload these as CI artifacts (see our Playwright GitHub Actions CI/CD guide for the full setup) so you can download and inspect them after a pipeline failure.

Tip: Playwright's HTML reporter marks tests that failed initially but passed on retry as "flaky." Run npx playwright show-report after a CI run to see a dedicated flaky test section. This is the fastest way to identify your flake-prone tests without any external tooling.

Fix Race Conditions with Proper Awaits

Race conditions are the number one cause of flaky Playwright tests. The fix is straightforward: never assert on something before Playwright has confirmed it is ready. Playwright's web-first assertions handle this automatically -- if you use them correctly.

Use Web-First Assertions

Web-first assertions automatically retry until the condition is met or the timeout expires. They are the single most important tool for preventing race conditions:

Correct vs. incorrect assertion patterns
// BAD: Reads text once, fails if element hasn't rendered yet
const text = await page.textContent('.status');
expect(text).toBe('Order confirmed');

// GOOD: Retries automatically until text appears or timeout
await expect(page.locator('.status')).toHaveText('Order confirmed');

// BAD: Checks visibility once at a single point in time
const isVisible = await page.isVisible('.modal');
expect(isVisible).toBeTruthy();

// GOOD: Waits for element to become visible
await expect(page.locator('.modal')).toBeVisible();

// BAD: Counts elements at a single moment
const count = await page.locator('.item').count();
expect(count).toBe(5);

// GOOD: Retries count until it matches
await expect(page.locator('.item')).toHaveCount(5);

The pattern is simple: if you are asserting on DOM state, always use expect(locator) methods (toBeVisible, toHaveText, toHaveCount, toHaveAttribute) rather than reading values with page.textContent() or locator.count() and asserting on the result. The former retries. The latter does not.

Wait for Network Responses

When a user action triggers an API call that updates the UI, wait for that API call to complete before asserting on the result:

Wait for API before asserting
// Wait for the API response, THEN assert on the UI
const responsePromise = page.waitForResponse(
  resp => resp.url().includes('/api/orders') && resp.status() === 200
);

await page.getByRole('button', { name: 'Place Order' }).click();
await responsePromise;

// Now it's safe to assert -- the API has responded
await expect(page.locator('.order-status')).toHaveText('Confirmed');

Warning: Never use page.waitForTimeout() to fix race conditions. It is always either too slow (wasting CI time) or too fast (still flaky). Use waitForResponse, waitForLoadState, or web-first assertions instead. These wait for the exact condition you need -- no more, no less. See our Playwright timeout errors fix guide for detailed timeout management strategies.

Eliminate Shared State Between Tests

Every Playwright test should be able to run independently, in any order, and produce the same result. If your tests depend on each other or modify shared resources, you are building a house of cards.

Use Fresh Browser Contexts

Playwright creates a new browser context for each test by default. This gives each test its own cookies, local storage, and session state. Do not fight this default by sharing contexts between tests. If a test needs authenticated state, use Playwright's storage state feature to load pre-saved authentication rather than relying on a previous test to have logged in:

playwright.config.ts -- per-project auth state
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    // Setup project: runs once, saves auth state
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },
    // Tests use saved auth state -- no login dependency
    {
      name: 'chromium',
      use: {
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

Isolate Test Data

If your tests create, modify, or delete data in a database or API, each test should work with its own unique data. Techniques include:

  • Generate unique identifiers -- use test.info().testId or a UUID as part of created resource names so tests never collide.
  • Use API setup/teardown -- create test data via API in a beforeEach hook and delete it in afterEach, rather than relying on a pre-populated database.
  • Snapshot and restore -- for database-heavy suites, snapshot the database before the suite and restore it after, ensuring a clean state for every run.
  • Use test fixtures -- Playwright's fixture system lets you define reusable setup/teardown logic that runs per-test. See our Playwright fixtures and hooks guide for the complete pattern.

The goal is simple: every test must be able to run in complete isolation. If you can run any single test from your suite with npx playwright test tests/my-test.spec.ts and it passes, your isolation is correct.

Configure Smart Retry Strategies

Retries are a pragmatic safety net. In the real world, CI environments have transient issues -- a DNS hiccup, a brief CPU spike, a Docker container recycling. Retries absorb this environmental noise without requiring you to over-engineer every test.

The key principle: retries are for environmental variance, not for masking test bugs. If a test needs retries to pass, you should fix it. But while you are fixing it, retries keep your pipeline green and your team productive.

Global Retries in Config

Set retries only in CI to keep local development fast:

playwright.config.ts -- CI-only retries
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // 0 retries locally, 2 in CI
  retries: process.env.CI ? 2 : 0,

  // Per-project overrides for known-stable vs. new tests
  projects: [
    {
      name: 'stable-suite',
      testDir: './tests/stable',
      retries: process.env.CI ? 1 : 0,
    },
    {
      name: 'new-features',
      testDir: './tests/new',
      retries: process.env.CI ? 3 : 0,
    },
  ],
});

With this setup, stable tests get one retry (they should rarely need it), while newly added tests get three retries as you stabilize them. As tests prove reliable, move them from the new-features project to the stable-suite project.

Monitor your flaky rate: Track the percentage of tests that pass only on retry. A healthy suite has a flaky rate below 1%. If you are above 5%, stop adding new tests and focus on stabilizing existing ones. The Playwright HTML reporter makes this easy -- look for the "flaky" badge on test results.

CI-Specific Fixes

Many tests pass locally but fail in CI because the CI environment is fundamentally different from your development machine. Here are the most impactful CI-specific fixes.

Headed vs. Headless Differences

Headless browsers in CI behave slightly differently from headed browsers on your machine. Animations may not fire, GPU-accelerated rendering is disabled, and default viewport sizes differ. Ensure your playwright.config.ts sets explicit viewport dimensions:

  • Set use: { viewport: { width: 1280, height: 720 } } in your config to guarantee consistent dimensions across local and CI.
  • If your application uses animations that affect element positioning, add '--disable-animations' to launch args or use page.emulateMedia({ reducedMotion: 'reduce' }).
  • Test locally in headless mode regularly: npx playwright test --headed=false catches headless-specific issues before they reach CI.

Resource Constraints

CI runners typically have 2-4 CPU cores and 4-8GB RAM. Running too many parallel workers on a resource-constrained machine causes tests to timeout because the browser cannot render fast enough.

  • Set workers: process.env.CI ? 2 : undefined to limit parallelism in CI while using all available cores locally.
  • Increase the global test timeout for CI: timeout: process.env.CI ? 60000 : 30000.
  • Monitor CI runner CPU and memory usage. If you see consistent spikes above 90%, reduce workers or upgrade your runner.

Browser Installation

A frequent CI failure is "browser not found" because Playwright's browsers were not installed or were cached incorrectly. Always run npx playwright install --with-deps in your CI pipeline to ensure browsers and their system dependencies are present. For Docker-based CI, use Playwright's official Docker images which come pre-configured. See our Playwright Docker tutorial for the complete container setup.

Parallel Worker Interference

When running tests in parallel, each worker gets its own browser instance, but they all share the same filesystem and network. If tests write to the same file, listen on the same port, or depend on the same external service, parallel execution will cause failures. See our Playwright parallel testing guide for strategies to handle parallel execution safely.

Use Claude AI to Debug Flaky Tests

Debugging flaky tests is time-consuming because the failure is intermittent and the root cause is often subtle. Claude AI can accelerate diagnosis dramatically by analyzing your test code, error output, and trace data in seconds.

Paste Error + Test Code into Claude Code

When a test fails in CI, copy the error output and the test file, then paste them into Claude Code:

Claude Code prompt -- diagnose flaky test
# In Claude Code, paste your test and the CI error:
> This Playwright test passes locally but fails intermittently in
  CI with "Timeout 30000ms exceeded waiting for
  expect(locator).toHaveText()". Here is the test code and the
  full error. Diagnose the flakiness and fix it.

  [paste test code]
  [paste CI error log]

Claude will analyze the code for common flakiness patterns: missing awaits, assertions before API responses complete, shared state between tests, and hardcoded waits. It typically identifies the root cause in under 30 seconds and provides a corrected version of the test with explanations for each change.

Use the MCP Server for Live Diagnosis

For harder-to-diagnose flakiness, use the Playwright MCP server to let Claude interact with your application directly. Claude can navigate to the problematic page, observe the timing of elements appearing, and identify exactly where the race condition occurs. This is especially useful for flakiness caused by animations, lazy-loaded content, or complex state transitions that are hard to reason about from code alone.

Pro tip: Ask Claude to "review this entire test file for flakiness risks" even before a test starts failing. Claude will flag potential race conditions, shared state issues, and timing problems proactively, saving you from debugging them in CI later.

Prevention Checklist

The best flaky test is one that never gets written. Use this checklist when writing new Playwright tests to prevent flakiness from the start:

  1. Always use web-first assertions -- expect(locator).toBeVisible(), .toHaveText(), .toHaveCount(). Never read a value and assert on it separately.
  2. Never use page.waitForTimeout() -- wait for specific conditions instead: waitForResponse, waitForLoadState, waitForSelector.
  3. One test, one purpose -- each test should verify a single behavior. Long tests with many steps are more likely to encounter timing issues.
  4. Isolate test data -- create unique data per test, clean up after. Never depend on data created by another test.
  5. Use fresh browser contexts -- do not share cookies, storage, or sessions between tests. Use storageState for auth.
  6. Set explicit viewport sizes -- configure viewport in your config so tests behave identically across machines.
  7. Mock time-dependent values -- use page.clock to control dates and timers instead of asserting on real-time values.
  8. Run with --repeat-each=5 before merging -- catch flakiness in development, not in production CI.
  9. Limit parallel workers in CI -- match worker count to available CPU cores. Over-parallelization causes timeouts.
  10. Review traces on every CI failure -- do not rerun without investigating. Every flaky failure has a root cause worth fixing.

Print this checklist and put it in your team's PR review template. Every Playwright test should pass all ten items before it gets merged.


Build bulletproof CI/CD pipelines with Playwright

Hands-on Udemy course: test stability patterns, CI/CD configuration, parallel execution, and AI-powered debugging with Claude. Ship with confidence.

Enroll on Udemy →

Frequently Asked Questions

Why are my Playwright tests flaky only in CI?

CI environments have fewer CPU cores, less memory, and no GPU acceleration compared to your local machine. This means animations take longer, network requests are slower, and page rendering is delayed. Tests that pass locally with comfortable timing margins fail in CI because those margins disappear. The fix is to use web-first assertions like expect(locator).toBeVisible() instead of hardcoded waits, and to configure adequate resource limits in your CI pipeline.

Should I use retries to fix flaky tests?

Retries are a safety net, not a fix. Setting retries: 2 in your playwright.config.ts prevents a single timing glitch from failing your entire pipeline, but if a test needs retries to pass consistently, you should still investigate and fix the root cause. Use retries in CI to absorb environmental variance while you work on eliminating the underlying flakiness.

How do I find which tests are flaky?

Use Playwright's --repeat-each flag to run each test multiple times: npx playwright test --repeat-each=10. Tests that fail on some runs but pass on others are flaky. You can also enable retries and check the HTML report for tests marked as "flaky" (passed on retry). For large suites, track test results over time in your CI system to identify tests with intermittent failures.

Does parallel execution cause flakiness?

Parallel execution does not inherently cause flakiness, but it exposes shared state problems. If two tests modify the same database record, write to the same file, or depend on the same user account, they will interfere with each other when running in parallel. The fix is proper test isolation: each test should create its own data, use its own browser context, and clean up after itself.

Can AI help debug flaky Playwright tests?

Yes. Tools like Claude Code can analyze failing test output, trace files, and screenshots to diagnose flakiness patterns. Paste your test code and the CI error log into Claude Code, and it will identify race conditions, missing awaits, shared state issues, and timing problems. The Playwright MCP server lets Claude interact with your application directly to reproduce and fix the issue.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn

Playwright + Claude AI Course

Stop Guessing Why Tests Fail -- Learn Systematic Debugging

The course covers end-to-end CI/CD pipeline setup, test stability patterns that eliminate flakiness at the source, and AI-powered debugging with Claude Code. You will build a production-grade Playwright suite that runs reliably in every environment.

  • Configure Playwright for stable CI/CD execution with retries, tracing, and parallel sharding
  • Master web-first assertions and proper await patterns that prevent race conditions
  • Use Claude AI and the MCP server to diagnose and fix flaky tests in minutes
  • Build test isolation patterns with fixtures, storage state, and unique test data
Master Stable CI/CD Testing on Udemy →