Parallel failures that disappear when you run a single test are rarely random. In Playwright, they usually come from one of three places: shared fixture state, cached setup data that is not worker-safe, or real application behavior that only shows up under concurrency. The hard part is not reproducing the failure once. The hard part is proving which layer owns the bug.

If you need to debug Playwright tests with shared fixtures, start by assuming the test harness is guilty until you can eliminate it. That mindset saves time because parallel worker reuse can make a suite look like the app is broken when the actual problem is a fixture that leaks state across tests, a cache key that collides between workers, or setup code that creates the same account, token, or record twice.

What usually breaks first

Playwright runs tests in isolated browser contexts, but that does not make your whole suite isolated. The framework can still reuse worker processes, fixture objects, API helpers, and external resources if you configure them that way. When failures only happen under Playwright parallel workers, the first question is simple:

Is the failing test reading from a resource that another worker can also write to?

That resource may be obvious, like a shared user account, or subtle, like a cached API response stored under a filename that ignores the worker index.

Typical symptoms include:

  • A test passes alone, fails in a full suite run
  • The failure rate changes when workers changes in playwright.config
  • Order-dependent failures, where test A breaks only after test B
  • State that looks “stale”, such as missing rows, unexpected auth cookies, or reused test data
  • API setup that succeeds locally but collides in CI when multiple workers create the same seed object

Separate the layers before you change the code

I use a short triage order because it keeps you from “fixing” the wrong layer.

Run the same file under one worker and then multiple workers.

bash npx playwright test tests/orders.spec.ts –workers=1 npx playwright test tests/orders.spec.ts –workers=4

If the test only fails with more than one worker, that strongly suggests shared state outside the individual test. It does not prove the fixture is broken, but it narrows the search.

For extra signal, retry with shuffling disabled or a single file filtered by -g. If the failure only appears when unrelated tests run first, you probably have a leak between tests or between worker-scoped fixtures.

2. Identify the state boundary that is being crossed

Map every piece of setup to one of these buckets:

  • Test-scoped, recreated for each test
  • Worker-scoped, reused by tests in the same worker
  • Suite-global, reused across every worker
  • External, stored in the app, API, database, cache, filesystem, or third-party service

The dangerous cases are worker-scoped or suite-global data that was written as if it were test-scoped.

In Playwright, fixture scoping matters because a worker-scoped fixture is reused within the worker process. That is efficient, but it means anything mutable inside that fixture becomes shared state. If the fixture caches a user, token, or seeded API result and the test mutates it, later tests inherit the damage.

3. Check for collisions in setup data

Cached API setup is often where parallel failures begin. Common examples:

  • Creating one account per suite, then every worker tries to use the same account
  • Reusing the same database row key or email address
  • Writing cache files to the same path
  • Storing auth state in one file per project instead of one per worker

A reliable sign of collision is nondeterministic setup failure before the UI even loads. If the failure appears in beforeAll, API seeding, or auth bootstrap, the browser may be innocent.

Shared fixtures: when reuse is safe, and when it is not

Shared fixtures are fine when they are read-only or when their mutable parts are partitioned per worker. They are not fine when a test can alter them in ways that affect another test.

A safer pattern is to isolate any mutable resource by worker index.

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

export const test = base.extend<{ apiUser: string }>({ apiUser: [async ({}, use, workerInfo) => {

const email = `e2e+worker-${workerInfo.workerIndex}@example.com`;
    await use(email);
  }, { scope: 'worker' }],
});

That example does not solve everything, but it shows the key rule: if a fixture can be changed, make its identity unique per worker or per test.

A few implementation checks help here:

  • Do not cache mutable objects in module scope unless they are truly read-only
  • Do not reuse the same email, username, or tenant identifier across workers
  • Do not assume one beforeAll is safe for data that multiple workers will mutate
  • Treat fixture cleanup as part of the contract, not an optional extra

If a fixture speeds up the suite by sharing setup, it also increases the cost of any leak. The more expensive the setup, the more disciplined the isolation must be.

Cached API setup: the fastest path to flaky parallel runs

API bootstrap is convenient, but cached setup can hide a concurrency bug for a long time. The usual pattern is a helper that logs in once, seeds data once, or fetches a token once, then reuses the result for the entire suite.

That works until parallel workers hit the same cache at the same time.

Things to verify:

  • Cache key includes workerIndex if the cached object is mutable or consumed once
  • Cache key includes environment and project if you run multiple browsers or CI jobs
  • Cache file path is unique per worker, not just per branch
  • Setup and teardown agree on ownership of the created data

A common anti-pattern is this shape:

let authStatePath = '.auth/state.json';

That path is fine only if a single process owns it. Under parallel workers, it can become a race condition, especially when tests update session state or when one worker deletes the file while another is reading it.

A safer pattern is to derive paths from worker identity.

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

export const test = base.extend({ storageState: async ({}, use, workerInfo) => {

const statePath = path.join('tmp', `.auth-${workerInfo.workerIndex}.json`);
    await use(statePath);
  },
});

The exact implementation will vary, but the principle should not: every mutable cache needs a deterministic owner.

A debugging workflow that separates real defects from fixture leakage

When I want to debug Playwright tests with shared fixtures, I use a small decision sequence.

Step 1: Minimize parallelism

Set workers to 1, then increase gradually. If the failure starts at 2 workers, you have a concurrency boundary. If it still fails at 1 worker, the problem is probably not worker contention.

Step 2: Remove all nonessential setup

Temporarily bypass fixture reuse, cached auth, and shared seed data. Create the minimum API state inside the test or in a dedicated helper that is obviously per-test.

This tells you whether the test itself is stable when it owns its own data.

Step 3: Add observability to the setup path

Log the worker index, resource IDs, and cache paths. A tiny amount of tracing is often enough to prove a collision.

console.log({
  workerIndex: workerInfo.workerIndex,
  project: testInfo.project.name,
  cachePath: statePath,
});

If two workers print the same ownership data, you already found the bug class.

Step 4: Delete hidden dependencies

Look for setup that depends on test order, test filenames, or incidental database contents. Parallel execution changes timing and reveal assumptions you did not mean to make.

Step 5: Re-run in CI-like conditions

Local repros matter, but CI failures often involve a different filesystem, slower API responses, different browser concurrency, or more limited resources. Use the same container image, environment variables, and worker count that CI uses.

When it is probably a real app defect

Not every parallel-only failure is a fixture problem. Sometimes the app truly breaks under concurrent access.

I would suspect the application when:

  • Two separate workers create different test data, but one record disappears
  • The same API call is idempotent in the test, but the backend duplicates or corrupts state
  • The UI shows stale data even after the test uses unique identifiers
  • Errors appear in logs from the application service, not only in the test runner

A useful distinction is this: if unique data still collides, the defect is probably in app logic, database constraints, or backend caching. If shared data collides, the defect may still be in the app, but the test harness is exposing it through poor isolation.

A compact diagnosis table

Symptom Most likely cause First check
Passes alone, fails with --workers=4 Shared fixture or worker-level state Fixture scope and cache keys
Fails only in beforeAll Shared API setup collision Unique IDs and setup ownership
Fails after another test ran State leak between tests Mutable globals and cleanup
Fails only in CI Environment-specific cache or timing Paths, permissions, worker count
Different error each run Race condition in setup or teardown Logging worker index and resource IDs

Hardening patterns that pay off later

Once you find the bug, fix the root cause and then make the failure harder to reintroduce.

Partition all mutable test data

Use unique identifiers per worker or per test. This includes:

  • Email addresses
  • Usernames
  • Order numbers
  • Database rows
  • Temp files
  • Auth state files

Keep worker-scoped fixtures read-only when possible

If a worker-scoped fixture only returns configuration, not live mutable state, it is much safer.

Make teardown explicit

Cleanup should remove the exact data created by the setup path. Avoid broad deletion queries that can erase another worker’s data.

Treat cached auth as disposable

If login state is cheap to create, do not optimize it into a shared file too early. The small time saved by reuse is easy to lose when debugging a flaky cache collision.

Prefer deterministic ownership over clever reuse

If you cannot explain which worker owns a resource, the resource is too shared.

A short checklist I use before marking the test fixed

  • The failure is reproducible with a known worker count
  • The cache key or resource name includes worker ownership where needed
  • Shared fixtures are read-only or explicitly partitioned
  • Teardown removes only the data created by that test or worker
  • The suite passes in the CI container, not just on a laptop
  • A rerun does not depend on test order

Who should skip the shared-fixture approach

You should avoid aggressive fixture reuse when:

  • Tests mutate the same account, tenant, or seed data
  • Your CI runs multiple projects or browsers against the same backend environment
  • Your API setup is fast enough to recreate per test
  • The cost of one flaky failure is higher than the time saved by reuse

In those cases, the maintenance cost of shared state is usually higher than the setup cost you were trying to save.

Final judgment

If a Playwright test fails only when parallel workers reuse shared fixtures or cached API setup, the safest default is to assume a state boundary problem, not a random browser failure. Start by isolating the mutable resource, then make the worker ownership visible in code. If the test still fails after you remove shared state, you have a much stronger case for a genuine application defect.

The key is not to eliminate reuse everywhere. The key is to reuse only what is provably read-only, and to make every mutable dependency uniquely owned.

FAQ

Why do Playwright tests pass locally but fail in CI with parallel workers?

Usually because CI changes the worker count, timing, filesystem behavior, or environment isolation. That can expose fixture leakage, cache collisions, or setup code that assumes one writer.

Should I use beforeAll for shared API setup?

Only if the data created there is read-only for the lifetime of the worker or file. If tests mutate that setup, beforeAll becomes a shared-state risk.

What is the safest way to cache login state?

Make the cache disposable and worker-specific if it can be mutated or consumed once. If you share one auth file across workers, you need to be sure no test can corrupt it.

How do I tell fixture leakage from a real app bug?

If the failure disappears when you give every worker unique data, the test harness was likely the problem. If unique data still collides or disappears, investigate app-side concurrency, persistence, or caching.

Does disabling parallelism solve the issue?

It can hide the symptom, but it does not fix the cause. Use single-worker runs as a debugging step, not as the final repair.

What should I log first when a parallel-only failure appears?

Log worker index, project name, cache path, created entity IDs, and any shared account or tenant identifier. That usually reveals whether two workers are touching the same resource.