Introduction
Imagine your enterprise Playwright regression suite contains 600 tests. You run them with a single worker and every one passes. Then your CI pipeline runs the same suite with 10 workers — and 37 tests fail. Rerun the failures and most of them pass. The next CI run fails 22 completely different tests.
npx playwright test --workers=1
# 600 passed, 0 failed
npx playwright test --workers=10
# 563 passed, 37 failedYour first reaction may be: "Playwright parallel execution is flaky." Usually, that is not the real problem. Parallel execution has exposed problems that were already hidden inside your test architecture.
- Shared test data
- Shared user accounts
- Shared database records
- Shared browser state
- Test dependencies
- Race conditions
- Resource contention
- Environment limitations
- API rate limits
- Incorrect cleanup
- Global mutable state
- Non-unique file names
1. First Understand What a Playwright Worker Is
Playwright Test executes tests using worker processes. A simplified execution model looks like this:
Playwright Test Runner
│
├── Worker 1
│ ├── Test 1
│ ├── Test 2
│ └── Test 3
│
├── Worker 2
│ ├── Test 4
│ ├── Test 5
│ └── Test 6
│
└── Worker 3
├── Test 7
├── Test 8
└── Test 9With --workers=1, tests execute one after another — Test A finishes, then Test B starts. With --workers=10, ten tests may interact with the application at approximately the same time. That changes everything.
2. Why Does One Worker Hide Problems?
Imagine three tests: Test A updates product P1001, Test B deletes product P1001, and Test C verifies product P1001. With one worker they run in a predictable sequence and may pass. Run them simultaneously:
Worker 1 Worker 2 Worker 3
Update P1001 Delete P1001 Read P1001
│ │ │
└──────────── race condition ─────────────┘
Possible result:
Worker 1 → 404
Worker 2 → PASS
Worker 3 → wrong product stateThe tests were never truly independent. Serial execution simply hid the problem.
3. The Golden Rule of Parallel Playwright Testing
Ideally, Test A should not depend on Test B, and Test A should not modify resources that Test B expects to remain unchanged. Each test should own its state:
Test A → Own State
Test B → Own State
Test C → Own State
NOT:
Shared State
↑
│
Test A ───────┼────── Test B
│
↓
Test CThis principle becomes critical as an enterprise regression suite grows.
4. Cause #1 — Shared Test Data
This is one of the most common reasons parallel Playwright tests fail. Suppose your framework contains a shared customer:
export const testCustomer = {
id: 'C1001',
email: 'automation@company.com',
status: 'ACTIVE'
};Now 20 tests use the same customer. One updates its status to INACTIVE, another deletes it, another expects it to be ACTIVE. Run serially and the suite may survive because setup happens in a predictable sequence. Run concurrently and the tests interfere with each other.
Worker 1 → Update C1001
Worker 2 → Delete C1001
Worker 3 → Read C1001
Worker 4 → Create Order for C10015. Better Solution — Unique Test Data
Instead of a fixed email, generate unique data for every test:
const customer = {
email: `automation-${crypto.randomUUID()}@example.test`
};
// or use a factory:
const customer = CustomerFactory.create();Worker 1 → Customer A (automation-a81f@example.test)
Worker 2 → Customer B (automation-f273@example.test)
Worker 3 → Customer C (automation-c491@example.test)
instead of:
Worker 1 ─┐
Worker 2 ─┼→ Customer C1001
Worker 3 ─┘6. Cause #2 — Shared User Accounts
This problem is especially common in enterprise applications. Suppose every test logs in as automation-user@company.com. With one worker: login, test, logout, login, test — everything works. Now run 10 workers against the same account:
Worker 1 ─┐
Worker 2 ─┤
Worker 3 ─┤
Worker 4 ─┼→ Same Account
Worker 5 ─┤
... │
Worker 10 ┘The application may enforce one active session per account. Worker 1 logs in, then Worker 2 logs in, and the server invalidates Worker 1's session. Worker 1 suddenly gets a 401 Unauthorized or is redirected to /login. The test appears flaky — but the real problem is that the authentication strategy is not parallel-safe.
7. Better Solution — Worker-Specific Accounts
For applications that modify server-side state, provide a different account per worker. Playwright's worker-scoped fixtures and worker information support this pattern:
account: [
async ({}, use, workerInfo) => {
const account = accounts[workerInfo.workerIndex];
await use(account);
},
{ scope: 'worker' }
]Worker 0 → automation-user-0
Worker 1 → automation-user-1
Worker 2 → automation-user-2
Worker 3 → automation-user-3
Worker
↓
Dedicated Account
↓
Dedicated Authentication State
↓
Tests8. Shared storageState Is Not Always the Problem
If all tests use the same storageState file, is that automatically wrong? No. If tests only read data and the application allows multiple simultaneous sessions, sharing authenticated state may be perfectly reasonable.
9. Example: Shared Shopping Cart
Test A adds a laptop to the cart; Test B adds a phone — both as customer@test.com. Serial execution passes because each test clears the cart first. In parallel, one worker may clear the other's cart, or the cart may contain both items when an assertion expects exactly one. Assertions begin failing randomly.
Worker 1 Worker 2
Clear Cart Clear Cart
↓ ↓
Add Laptop Add Phone
↓ ↓
Validate 1 item Validate 1 item
Actual cart: Laptop + Phone (or empty)10. Cause #3 — Shared Database Records
Enterprise Playwright frameworks frequently validate backend databases. If several tests modify the same record simultaneously, the last write wins:
-- Worker 1
UPDATE products SET quantity = 10 WHERE product_id = 'P1001';
-- Worker 2
UPDATE products SET quantity = 20 WHERE product_id = 'P1001';Worker 1 expects quantity = 10 but receives 20 because Worker 2 updated the same record. Again: Playwright is not flaky — your data is shared.
11. Use Unique Database Records
Give every test — or every worker — its own records, created through the API and cleaned up afterwards:
const product = await productService.createProduct({
name: `PW-${crypto.randomUUID()}`,
quantity: 10
});Test
↓
Generate unique ID
↓
Create record through API
↓
Execute test
↓
Validate DB
↓
Delete record12. Cause #4 — Incorrect Cleanup
Consider a convenient-looking cleanup hook:
test.afterEach(async () => {
await dbClient.deleteAllTestProducts();
});With 10 workers, Worker 2 finishes its test and deletes ALL test products — including the ones Worker 1 is still using. Worker 1 fails. This is a classic enterprise parallel-execution bug.
13. Cleanup Only What the Test Owns
Instead of deleting everything, delete exactly the resource the test created:
await dbClient.deleteProduct(product.id);
// Think:
// Create Own Resource → Use Own Resource → Delete Own Resource
// NOT: Create Data → Delete Everyone's Data14. Cause #5 — Tests Depend on Execution Order
test('create customer', async () => { /* create C1001 */ });
test('update customer', async () => { /* update C1001 */ });
test('delete customer', async () => { /* delete C1001 */ });This secretly assumes Test 1 → Test 2 → Test 3. A parallel runner may execute operations simultaneously or in a different order — producing 404s, missing customers, and wrong state.
15. Better Test Design
Each test creates its own prerequisite. Create, update, validate and cleanup belong to one test:
test('update customer', async ({ customerService }) => {
const customer = await customerService.createCustomer();
await customerService.updateCustomer(customer.id, {
status: 'INACTIVE'
});
// assertions
});16. What If It Really Is One Business Workflow?
Sometimes Create Order → Approve Order → Ship Order → Return Order is intentionally one stateful workflow. Don't turn every step into a separate test that depends on previous tests — make it one test that owns the whole workflow:
test('order lifecycle', async ({ orderService }) => {
const order = await orderService.create();
await orderService.approve(order.id);
await orderService.ship(order.id);
// validations
});17. Cause #6 — Global Mutable Variables
let customerId: string;
test('create customer', async () => {
customerId = await createCustomer();
});
test('validate customer', async () => {
await validateCustomer(customerId);
});Test 2 depends on state produced by Test 1 — and it gets worse when multiple tests modify shared module-level variables. Keep state inside the test or an appropriately scoped fixture.
18. Cause #7 — Shared Files
If 10 tests download report.csv into the same folder, parallel execution means overwrites, file locking, wrong content and deletion conflicts:
Worker 1 → downloads/report.csv
Worker 2 → downloads/report.csv
Worker 3 → downloads/report.csv
Worker 4 → downloads/report.csv19. Use Unique File Names
const fileName = `report-${crypto.randomUUID()}.csv`;Or use worker-specific directories: downloads/worker-0/, downloads/worker-1/, and so on. The same principle applies to screenshots, PDFs, CSV exports, temporary JSON, test artifacts and generated payloads.
20. Cause #8 — External Service Rate Limits
Sometimes the tests are perfectly isolated, but the environment cannot handle the traffic. One worker might generate 10 API requests per second; ten workers generate 100+. An external service may respond with 429 Too Many Requests or 503 Service Unavailable.
- Payment gateways
- Email services
- Identity providers
- Third-party APIs
- Cloud APIs
- Internal shared QA services
This is not necessarily a Playwright issue — it may be an environment-capacity issue.
21. Cause #9 — Database Connection Limits
If each worker creates 5 DB connections, 30 workers need 150 connections. The QA database may only support a limited pool, and you start seeing connection timeouts, "too many connections", pool exhaustion and ECONNRESET.
The solution may involve connection pooling, worker-scoped clients, proper connection cleanup, reduced workers, or infrastructure scaling — not random retries.
22. Cause #10 — Backend Contention
Parallelism can overwhelm a shared QA environment that is much smaller than production:
20 Playwright Workers
│
▼
API Gateway
│
▼
Order API
│
▼
Kafka
│
▼
Order Service
│
▼
PostgreSQLTwenty workers may create CPU contention, DB contention, queue backlog, API timeouts, slow UI responses and event-processing delays. Tests then fail on "Timeout 30s exceeded". Raising the timeout to 120 seconds may hide the symptom but does not solve the architecture problem.
23. Cause #11 — Asynchronous Event Collisions
In an event-driven system, if every test uses productId P1001, parallel execution may produce CREATED, UPDATED, DELETED and CREATED events for P1001 almost simultaneously. Your event validator may pick up the wrong event. Use unique correlation identifiers:
const correlationId = crypto.randomUUID();
// Test → correlationId → REST request → Kafka event → DB record → ValidationThis allows each worker to identify and validate its own workflow end-to-end.
24. Cause #12 — Shared Feature Flags or Environment Configuration
If Test A enables FEATURE_X while Test B expects it disabled, serial execution may hide the issue behind setup and cleanup — but in parallel, Worker 2 fails the moment Worker 1 flips the flag. Global environment configuration is dangerous to modify during parallel tests:
- Feature flags
- Tenant configuration
- System preferences
- Global business dates
- Shared configuration records
- Environment switches
25. Cause #13 — Shared Tenant or Organization
Enterprise SaaS applications often scope data to a tenant: users, orders, configuration and permissions. If 50 tests modify Tenant A simultaneously, they may conflict even if each test uses a different user. Isolation may need to happen at a higher level:
Worker 1 → Tenant 1
Worker 2 → Tenant 2
Worker 3 → Tenant 3
not merely:
Worker 1 → User 1
Worker 2 → User 226. Cause #14 — Unique Constraints
If every test creates a user with the same username and email, serial execution survives because each test deletes it afterwards. With 10 workers inserting simultaneously, the database answers with a UNIQUE constraint violation. The fix is not retrying the test — use unique data.
27. Cause #15 — Cleanup Happens Too Early
If a test creates a customer, starts an async backend process, completes, and immediately deletes the customer — the Kafka consumer that still needs that record now finds it gone. Parallel execution makes timing issues like this much easier to expose. Cleanup should respect the actual application lifecycle.
28. Why Retries Make This More Confusing
With retries: 2, a test that fails on the first attempt may pass on retry, and the report marks it FLAKY. The team concludes "the UI is unstable." But perhaps the first attempt collided with another worker — by retry time the other test had finished, the shared record was available, or the rate limit had cleared. Retries can hide concurrency problems.
29. How to Prove You Have a Parallelism Problem
A very useful debugging technique — increase workers gradually and watch the failure trend:
npx playwright test --workers=1 # 600 passed
npx playwright test --workers=2 # 598 passed, 2 failed
npx playwright test --workers=5 # 587 passed, 13 failed
npx playwright test --workers=10 # 563 passed, 37 failedWorkers go up, failures go up — a strong signal. Investigate shared state, data collisions, account conflicts, environment capacity, rate limits and resource exhaustion before blaming locators.
30. The Most Important Debugging Question
Check: user, customer, product, order, database row, tenant, cart, file, feature flag, API token, environment configuration, Kafka message, external service. That question often finds the root cause surprisingly quickly.
31. Use Worker Information During Investigation
Playwright exposes worker information that you can include in logs and generated test data:
test('example', async ({ page }, testInfo) => {
console.log(`Worker: ${testInfo.workerIndex}`);
});
const email =
`pw-worker-${testInfo.workerIndex}-${Date.now()}@example.test`;Worker identifiers make parallel failures much easier to trace.
32. Better Enterprise Logging
Instead of logging "Creating customer", log the context that lets you trace a failure across the whole stack:
[Test: Create Premium Customer]
[Worker: 4]
[Customer: C-98214]
[Correlation: 6c31...]
Creating customer
Trace path when CI fails:
Test → Worker → Customer → API Request → Kafka Event → Database33. Worker-Scoped Resources
Some expensive resources can be created once per worker instead of once per test:
type WorkerFixtures = {
apiClient: ApiClient;
};
export const test = base.extend<{}, WorkerFixtures>({
apiClient: [
async ({}, use) => {
const client = await ApiClient.create();
await use(client);
await client.dispose();
},
{ scope: 'worker' }
]
});Each worker gets its own API client, dedicated account, tenant assignment or database client — reused by all tests within that worker, when designed carefully.
34. Test-Scoped vs Worker-Scoped Data
Test-scoped
Test A creates Customer A; Test B creates Customer B. Best when tests mutate data.
Worker-scoped
Worker 1 owns Account A, reused by Tests 1–3 in that worker. Useful when several tests can safely share an expensive resource within one worker.
Global/shared
All workers mutate the same customer. This is where trouble often starts.
35. Should You Simply Set workers: 1?
You could configure workers: 1 and make the failures disappear. But 600 tests × 30 seconds is 18,000 seconds — five hours. With safe parallelism, execution might drop dramatically. workers: 1 is often a workaround rather than the architectural solution.
36. But Maximum Parallelism Is Also Not the Goal
1 worker → 180 minutes
5 workers → 45 minutes
10 workers → 27 minutes
20 workers → 25 minutes
40 workers → 38 minutes ← slower againWhy did 40 workers become slower? CPU saturation, memory pressure, DB contention, network saturation, API throttling, queue backlog and browser overhead. The goal is to find the highest stable and efficient parallelism level for your system — not to use as many workers as possible.
37. Parallel-Safe Enterprise Architecture
Playwright Runner
│
├── Worker 1
│ ├── Account 1
│ ├── Customer 1
│ ├── Product 1
│ └── Correlation ID 1
│
├── Worker 2
│ ├── Account 2
│ ├── Customer 2
│ ├── Product 2
│ └── Correlation ID 2
│
└── Worker 3
├── Account 3
├── Customer 3
├── Product 3
└── Correlation ID 3Each execution unit owns the state it modifies.
38. A Better Test Data Factory
import crypto from 'crypto';
export class TestDataFactory {
static product(workerIndex: number) {
const id = crypto.randomUUID();
return {
productId: `PW-${workerIndex}-${id}`,
name: `Playwright Product ${id}`,
quantity: 10
};
}
}
// usage:
const product = TestDataFactory.product(testInfo.workerIndex);This gives you data that is both unique and traceable back to the worker that created it.
39. API-Based Test Setup
For large suites, UI setup is expensive. Instead of driving the browser through login and multiple creation forms before the actual test, create prerequisites through the API:
API
↓
Create Customer
↓
Create Product
↓
Create Order
↓
Browser
↓
Test behaviorIndependent test setup becomes fast enough that each parallel test can realistically own its own data.
40. Cleanup Strategy for Parallel Tests
let customerId: string;
test.beforeEach(async ({ customerService }) => {
const customer = await customerService.create();
customerId = customer.id;
});
test.afterEach(async ({ customerService }) => {
if (customerId) {
await customerService.delete(customerId);
}
});Generate a unique resource, store its ID, execute the test, clean up only that ID. Even better, encapsulate the resource lifecycle in fixtures so tests do not manage setup and cleanup themselves.
41. Parallel-Safe Fixture Example
customer: async ({ customerService }, use) => {
const customer = await customerService.create({
email: `pw-${crypto.randomUUID()}@example.test`
});
await use(customer);
await customerService.delete(customer.id);
}test('customer can update profile', async ({ customer, customerPage }) => {
await customerPage.open(customer.id);
// test behavior
});The fixture owns creation, provides the test resource, and guarantees cleanup — a powerful enterprise pattern.
42. Serial Mode Still Has Legitimate Uses
Not every scenario should run in parallel. Some workflows genuinely share state — create a complex dataset, then validate step 1, step 2, step 3. Playwright supports serial execution patterns, but use them intentionally. Do not make an entire 600-test suite serial because a few workflows require ordered state; isolate those cases.
43. Parallelism and Test Design Are Connected
Parallel execution is not merely a CI optimization — it tests the quality of your framework architecture. If increasing workers exposes dozens of failures, it may reveal hidden dependencies, shared state, weak data design, poor cleanup, authentication coupling and environment limitations.
44. Real Enterprise Example
Imagine an order platform with tests for create, update, cancel, search, approve, ship and refund. Bad architecture — all tests hit ORDER-1001 as the same customer and user:
Create ─┐
Update ─┤
Cancel ─┼→ ORDER-1001 ← chaos
Ship ───┤
Refund ─┘
Better:
Create Test → ORDER-A
Update Test → ORDER-B
Cancel Test → ORDER-C
Ship Test → ORDER-DEach test creates exactly the state it requires.
45. Real Enterprise E2E Example
Consider a flow across the whole stack — Playwright UI → REST API → Order Service → Kafka → Payment Service → PostgreSQL → WebSocket → UI updated. For parallel testing, propagate a unique identifier through every hop:
Test ID
↓
Order ID
↓
Correlation ID
↓
Kafka Event
↓
DB Record
↓
WebSocket Event
Worker 1 → Correlation A
Worker 2 → Correlation B
Worker 3 → Correlation CYour test can then validate its own transaction instead of whichever event happened to arrive first.
46. How I Would Investigate the 600-Test Problem
With 600 passing at 1 worker and 37 failing at 10, I would not immediately edit the 37 tests. First, classify the failures:
37 failures
├── 12 Authentication
├── 8 Test Data
├── 6 Database
├── 4 Timeout
├── 3 File collision
├── 2 API 429
└── 2 UnknownNow patterns become visible. Maybe 12 authentication failures all use same-admin-user. Maybe eight data failures all use CUSTOMER-1001. Maybe six DB failures all modify subscription_status. You have discovered architectural root causes rather than 37 unrelated bugs.
47. Then Increase Parallelism Gradually
workers=1 → 600 passed
workers=2 → 600 passed
workers=5 → 600 passed
workers=10 → 600 passed
workers=15 → 596 passed ← investigate againPerhaps the application starts rate-limiting around that concurrency. Your practical stable configuration might be workers: process.env.CI ? 10 : undefined. The number should come from evidence, not guesswork.
48. Parallel-Test Investigation Checklist
Test design
- Does each test work independently?
- Does any test depend on another test?
- Does execution order matter?
- Is mutable global state used?
Test data
- Are IDs unique?
- Are emails unique?
- Are products, customers or orders shared?
Authentication
- Do workers share one account?
- Does login invalidate old sessions?
- Do tests modify user-specific state?
- Should accounts be worker-specific?
Database
- Are workers modifying the same rows?
- Is cleanup too broad?
- Are DB connections properly closed?
- Is the pool exhausted?
Files
- Are downloads using identical names?
- Are workers writing to the same directory?
- Are temporary files shared?
Backend
- Are APIs being rate-limited?
- Is the QA environment overloaded?
- Is Kafka processing delayed?
- Are asynchronous events colliding?
CI
- Enough CPU, memory and browser resources?
- Correct worker count?
Cleanup
- Does each test delete only its own data?
- Can cleanup affect another running test?
- Does cleanup happen before async processing finishes?
49. What Not to Do
- Don't immediately increase the timeout from 30s to 120s without understanding why tests became slow.
- Don't immediately add retries: 5 — it may hide concurrency problems.
- Don't disable parallelism permanently with workers: 1 — it hides poor architecture and makes regression unnecessarily slow.
- Don't blindly make everything serial — fix isolation where possible.
- Don't blame Playwright first — a browser test can fail because of the application, database, API, authentication, Kafka, network, test data or infrastructure. Playwright may simply be exposing the problem.
50. The Architecture Principle
Your goal is to move from shared everything to owned everything:
FROM:
Shared User
│
Shared Data
│
Shared State
│
┌────────┼────────┐
▼ ▼ ▼
Worker 1 Worker 2 Worker 3
TO:
Worker 1 Worker 2 Worker 3
│ │ │
User A User B User C
│ │ │
Data A Data B Data C
│ │ │
State A State B State CThat is the foundation of scalable Playwright automation.
Final Takeaway
When 600 tests pass with one worker but 37 fail with ten, do not conclude that parallel Playwright tests are flaky. Parallel execution has probably revealed something important. Look for shared test data, shared accounts, shared DB records, test-order dependencies, global mutable state, file collisions, incorrect cleanup, rate limits, resource exhaustion, async event collisions, shared tenant configuration and environment capacity.
The ideal enterprise design:
Test
↓
Own Authentication Context
↓
Own Test Data
↓
Own Resource IDs
↓
Own Correlation ID
↓
Execute
↓
Validate
↓
Cleanup Own ResourcesOnce a large Playwright suite is truly independent and parallel-safe, increasing workers stops being a dangerous switch and becomes what it should be — a controlled way to scale enterprise regression execution.
Complete professional Playwright + TypeScript framework covering UI, REST API, GraphQL, WebSocket, Kafka, Database, E2E and Agentic AI testing — with a complete AUT included.
Get Playwright tutorials in your inbox
Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.
Playwright Framework Series
View all →- 1Getting Started with Playwright: Installation, Setup, and Your First Test
- 2Playwright Locators: The Complete Guide with Real-World Examples
- 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
- 4Playwright Assertions: The Complete Guide with Real-World Examples
- 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
- 6Playwright Fixtures: The Complete Guide with Real-World Examples
- 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
- 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
- 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
- 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
- 11Page Object Model with Playwright: A Practical Guide
- 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
- 13Building an Enterprise Playwright Framework from Scratch
API Testing Series
View all →- 1Playwright API Testing: The Complete Guide with Real-World Examples
- 2Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
- 3Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
- 4Part 3: Mastering CRUD Operations in Playwright API Testing
- 5Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
- 6Part 5: Building an Enterprise-Level Playwright API Automation Framework
- 7Part 6: API Models, Schema Validation & Test Data Management in Playwright
- 8Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
- 9Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
- 10Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
- 11Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
- 12API Testing with Playwright: Request Context, Auth, and Assertions
- 13Playwright Backend Testing Tutorial – REST API, GraphQL, WebSocket, Kafka, Database & Microservices