Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/microsoft/playwright/llms.txt

Use this file to discover all available pages before exploring further.

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