Skip to main content

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:
  1. Test runner spawns multiple worker processes
  2. Each worker runs tests sequentially
  3. Workers run in parallel to each other
  4. 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
  },
]

Configure Mode

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, 1s each   -> Worker 1
  medium-test.spec.ts     # 3 tests, 10s each  -> Worker 2
  slow-test.spec.ts       # 1 test, 30s        -> 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
  });
});

Performance Optimization

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

  1. Design for parallelization: Make tests independent
  2. Use appropriate worker count: Balance speed and resources
  3. Avoid shared state: Each test should be self-contained
  4. Use worker-scoped fixtures: For expensive setup
  5. Leverage sharding: For very large test suites
  6. Monitor CI resources: Adjust workers based on available CPU
  7. 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

Build docs developers (and LLMs) love