Introduction
Playwright Test runs tests in parallel by default using multiple worker processes. This significantly reduces test execution time.
How Parallelization Works
From src/common/config.ts:121,240-252 and src/runner/testRunner.ts:88-115:
Playwright uses a worker-based parallelization model :
Test runner spawns multiple worker processes
Each worker runs tests sequentially
Workers run in parallel to each other
Tests are distributed across available workers
export default defineConfig ({
workers: 4 , // Run 4 tests in parallel
}) ;
Worker Configuration
Fixed Number of Workers
export default defineConfig ({
workers: 4 , // Always use 4 workers
}) ;
Percentage of CPU Cores
From src/common/config.ts:240-252:
export default defineConfig ({
workers: '50%' , // Use 50% of available CPU cores
}) ;
// Implementation:
function resolveWorkers ( workers : string | number ) : number {
if ( typeof workers === 'string' && workers . endsWith ( '%' )) {
const cpus = os . cpus (). length ;
return Math . max ( 1 , Math . floor ( cpus * ( parseInt ( workers , 10 ) / 100 )));
}
return workers ;
}
Automatic Detection
export default defineConfig ({
workers: undefined , // Auto-detect based on CPU cores
}) ;
Environment-Based Configuration
export default defineConfig ({
workers: process . env . CI ? 2 : undefined ,
// Use 2 workers in CI, auto-detect locally
}) ;
Parallel Modes
From src/common/test.ts:58 and src/common/testType.ts:46-51,155-168:
Default Mode
Tests within a file run sequentially:
test ( 'test 1' , async ({ page }) => {});
test ( 'test 2' , async ({ page }) => {});
// test 2 waits for test 1 to complete
Different files run in parallel:
tests/
login.spec.ts # Runs in worker 1
checkout.spec.ts # Runs in worker 2
search.spec.ts # Runs in worker 3
Fully Parallel Mode
Run all tests in parallel:
From src/common/config.ts:102:
export default defineConfig ({
fullyParallel: true ,
}) ;
Or per-project:
projects : [
{
name: 'chromium' ,
use: { browserName: 'chromium' },
fullyParallel: true ,
},
]
Describe Parallel
Make specific describe blocks run in parallel:
From src/common/testType.ts:47,155-158:
test . describe . parallel ( 'Parallel suite' , () => {
test ( 'test 1' , async ({ page }) => {});
test ( 'test 2' , async ({ page }) => {});
test ( 'test 3' , async ({ page }) => {});
// All three tests run in parallel
});
Serial Mode
Force tests to run serially:
From src/common/testType.ts:49,155-156:
test . describe . serial ( 'Serial suite' , () => {
test ( 'test 1' , async ({ page }) => {});
test ( 'test 2' , async ({ page }) => {});
// test 2 waits for test 1
});
describe.parallel cannot be nested inside describe.serial or default mode describe blocks.
Worker Isolation
Each worker maintains its own isolated environment:
Worker-Scoped Fixtures
Shared within a worker, isolated between workers:
const test = base . extend ({
database: [ async ({}, use ) => {
const db = await createDatabase ();
await use ( db );
await db . close ();
}, { scope: 'worker' }],
});
test ( 'test 1' , async ({ database }) => {
// Uses same database as test 2 if in same worker
});
test ( 'test 2' , async ({ database }) => {
// Reuses database from test 1 if in same worker
});
Browser Instance Sharing
From src/index.ts:104-126:
Browser instances are shared within workers:
browser : [ async ({ playwright , browserName }) => {
const browser = await playwright [ browserName ]. launch ();
await use ( browser );
await browser . close ({ reason: 'Test ended.' });
}, { scope: 'worker' }]
Each worker:
Launches one browser instance
Creates fresh contexts for each test
Reuses browser across tests
Controlling Parallelization
Project-Level Workers
From src/common/config.ts:212-215:
projects : [
{
name: 'setup' ,
testMatch: / . * \. setup \. ts/ ,
workers: 1 , // Always use 1 worker for setup
},
{
name: 'chromium' ,
use: { browserName: 'chromium' },
workers: 4 , // Use 4 workers for chromium tests
},
]
From src/common/testType.ts:186-209:
test . describe ( 'Suite' , () => {
test . describe . configure ({ mode: 'parallel' });
test ( 'test 1' , async ({ page }) => {});
test ( 'test 2' , async ({ page }) => {});
// Both run in parallel
});
test . describe ( 'Serial Suite' , () => {
test . describe . configure ({ mode: 'serial' });
test ( 'test 1' , async ({ page }) => {});
test ( 'test 2' , async ({ page }) => {});
// Run serially
});
Worker Index
Access worker index in tests:
test ( 'test' , async ({ page }, testInfo ) => {
console . log ( `Running in worker ${ testInfo . workerIndex } ` );
// Use worker index for port allocation
const port = 3000 + testInfo . workerIndex ;
await page . goto ( `http://localhost: ${ port } ` );
});
Sharding
Distribute tests across multiple machines:
From src/common/config.ts:116:
Configuration
export default defineConfig ({
shard: { total: 3 , current: 1 } ,
}) ;
CLI Usage
# Machine 1
npx playwright test --shard=1/3
# Machine 2
npx playwright test --shard=2/3
# Machine 3
npx playwright test --shard=3/3
CI/CD Example
# GitHub Actions
strategy :
matrix :
shardIndex : [ 1 , 2 , 3 , 4 ]
shardTotal : [ 4 ]
steps :
- name : Run tests
run : npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
Test Distribution
Tests are distributed to balance execution time:
// Playwright automatically distributes:
tests /
fast - test . spec . ts # 5 tests , 1 s each -> Worker 1
medium - test . spec . ts # 3 tests , 10 s each -> Worker 2
slow - test . spec . ts # 1 test , 30 s -> Worker 3
Parallel Execution Patterns
Independent Tests
Tests that don’t share state:
export default defineConfig ({
fullyParallel: true , // All tests run in parallel
}) ;
test ( 'user login' , async ({ page }) => {
// Independent
});
test ( 'product search' , async ({ page }) => {
// Independent
});
Shared Setup with Parallel Tests
test . describe ( 'E-commerce' , () => {
test . beforeEach ( async ({ page }) => {
await page . goto ( '/' );
await loginUser ( page );
});
// These run in parallel if fullyParallel is true
test ( 'view cart' , async ({ page }) => {});
test ( 'view wishlist' , async ({ page }) => {});
test ( 'view orders' , async ({ page }) => {});
});
Sequential Test Flow
test . describe . serial ( 'Checkout flow' , () => {
test ( 'add to cart' , async ({ page }) => {
// Must run first
});
test ( 'enter shipping' , async ({ page }) => {
// Depends on cart state
});
test ( 'complete payment' , async ({ page }) => {
// Depends on shipping
});
});
Optimal Worker Count
// Too few workers: underutilized CPU
workers : 1
// Optimal: balance CPU usage and overhead
workers : os . cpus (). length
// Too many workers: excessive overhead
workers : os . cpus (). length * 4
Test Organization
// Good: Fast tests together
tests /
unit / # Fast tests , high parallelization
integration / # Medium tests , moderate parallelization
e2e / # Slow tests , lower parallelization
// Configure per directory
projects : [
{ testDir: './tests/unit' , workers: 8 },
{ testDir: './tests/integration' , workers: 4 },
{ testDir: './tests/e2e' , workers: 2 },
]
Browser Reuse
Worker-scoped browser fixture reuses browsers:
// Efficient: Browser launched once per worker
test ( 'test 1' , async ({ browser }) => {
const context = await browser . newContext ();
// ...
});
test ( 'test 2' , async ({ browser }) => {
// Reuses same browser from test 1
});
Debugging Parallel Tests
Run Tests Serially
npx playwright test --workers=1
View Worker Output
npx playwright test --workers=1 --reporter=line
Test Info Debugging
test ( 'debug info' , async ({ page }, testInfo ) => {
console . log ({
workerIndex: testInfo . workerIndex ,
parallelIndex: testInfo . parallelIndex ,
retry: testInfo . retry ,
});
});
Common Pitfalls
Shared Mutable State
// BAD: Shared state between tests
let userId : string ;
test ( 'create user' , async ({ request }) => {
const response = await request . post ( '/users' );
userId = ( await response . json ()). id ; // Race condition!
});
test ( 'delete user' , async ({ request }) => {
await request . delete ( `/users/ ${ userId } ` ); // May run before create!
});
// GOOD: Independent tests
test ( 'create user' , async ({ request }) => {
const response = await request . post ( '/users' );
const userId = ( await response . json ()). id ;
await request . delete ( `/users/ ${ userId } ` );
});
File System Operations
// BAD: Same file across tests
test ( 'test 1' , async () => {
fs . writeFileSync ( 'data.json' , '{}' ); // Conflict!
});
test ( 'test 2' , async () => {
fs . writeFileSync ( 'data.json' , '{}' ); // Conflict!
});
// GOOD: Worker-specific files
test ( 'test 1' , async ({ }, testInfo ) => {
const file = `data- ${ testInfo . workerIndex } .json` ;
fs . writeFileSync ( file , '{}' );
});
Database Conflicts
// BAD: Shared database records
test ( 'update user' , async ({ request }) => {
await request . put ( '/users/1' , { name: 'New Name' });
});
test ( 'delete user' , async ({ request }) => {
await request . delete ( '/users/1' ); // Conflicts with update!
});
// GOOD: Unique records per test
test ( 'update user' , async ({ request }) => {
const user = await createUniqueUser ();
await request . put ( `/users/ ${ user . id } ` , { name: 'New Name' });
});
Best Practices
Design for parallelization : Make tests independent
Use appropriate worker count : Balance speed and resources
Avoid shared state : Each test should be self-contained
Use worker-scoped fixtures : For expensive setup
Leverage sharding : For very large test suites
Monitor CI resources : Adjust workers based on available CPU
Use serial mode sparingly : Only when necessary
Example: Optimal Configuration
import { defineConfig } from '@playwright/test' ;
import os from 'os' ;
export default defineConfig ({
// Use 50% of CPUs locally, 100% in CI
workers: process . env . CI ? '100%' : '50%' ,
// Enable full parallelization
fullyParallel: true ,
// Limit failures to stop early
maxFailures: process . env . CI ? 10 : undefined ,
projects: [
{
name: 'setup' ,
testMatch: / . * \. setup \. ts/ ,
workers: 1 , // Setup runs serially
},
{
name: 'chromium' ,
dependencies: [ 'setup' ],
use: { browserName: 'chromium' },
// Inherits workers from global config
},
] ,
}) ;
Next Steps
Retries Learn about test retries
Configuration Configure worker settings