Back to Playwright & UI Automation
Q3Question 3 of 7

Running Playwright tests in parallel and choosing the right level of parallelism

Key Concepts
  • Playwright spawns multiple worker processes, each running tests in isolation with its own browser, sharding test files across workers
  • Set workers in config or command line; fullyParallel also runs tests within a file across workers
  • Tests must be independent: eliminate shared mutable data and order dependence before increasing parallelism
  • Isolate data per worker with unique keys so parallel tests do not contend on the same database rows
  • Reuse authentication state so workers start authenticated cheaply instead of swamping the auth service
  • More workers is counterproductive past the environment's capacity: the bottleneck causes contention, flakiness, and slower total time

Definition

Playwright parallelizes with isolated worker processes sharding tests, and you pick the worker count by balancing test independence, shared state contention, environment and database capacity, machine resources, and reusable auth, because once the environment is the bottleneck more workers only add contention, flakiness, and time.
  • Worker model – The runner spawns multiple worker processes, each executing tests in its own process with its own browser. Test files are sharded across workers.
  • Configuration – Set workers in config or CLI. Enable fullyParallel to run tests within a file across workers too.
  • Test independence – Tests must not depend on each other's state or order. Eliminate shared mutable data before scaling.
  • Data isolation – Use per-worker or per-test data with unique keys so parallel tests do not contend.
  • Auth reuse – Save storage state once and load it per worker, avoiding parallel login storms.
  • Bottleneck dynamic – Past the environment's capacity, more workers cause contention, timeouts, flakiness, and slower total time.

Think of running the National Bank of India test suite like opening checkout counters. Ten counters with plenty of staff serve customers fast. But if all ten share one tiny stockroom with a single clerk, opening more counters makes everyone wait longer. The workers are the counters and the test environment is the shared stockroom. More workers help only until the shared environment becomes the limit.

Understanding the Concepts

How does the worker-based model work?

Playwright starts a configurable number of worker processes. Each worker runs tests in its own process with its own browser, so workers are isolated by default.

TYPESCRIPT
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  workers: process.env.CI ? 4 : undefined,  // 4 in CI, auto locally
  fullyParallel: true, // Tests within a file also parallelize
});
BASH
# Override on command line
npx playwright test --workers=8

# Run a specific file serially
npx playwright test transfer.spec.ts --workers=1

How do you ensure test independence?

Parallelism is only safe if tests do not depend on each other's state or order.

TYPESCRIPT
// WRONG: tests share mutable state
let sharedBalance = 50000;
test('Transfer deducts', async ({ page }) => {
  sharedBalance -= 5000; // Another worker might read this
});

// CORRECT: each test creates its own data
test('Transfer deducts', async ({ page, request }) => {
  const accountId = `ACC-${Date.now()}-${Math.random().toString(36).slice(2)}`;
  await request.post('/api/test/accounts', {
    data: { id: accountId, balance: 50000 },
  });
  // Use accountId throughout this test
});

How do you isolate data per worker?

Fold the worker index into identifiers so workers never share mutable rows.

TYPESCRIPT
import { test, expect } from '@playwright/test';

test('Transfer with isolated data', async ({ page }, testInfo) => {
  const workerIndex = testInfo.workerIndex;
  const accountId = `ACC-W${workerIndex}-${Date.now()}`;

  // Seed data specific to this worker
  await page.request.post('/api/test/accounts', {
    data: { id: accountId, balance: 100000 },
  });

  // This account is unique to this worker
  await page.goto(`/accounts/${accountId}`);
});

How do you reuse authentication state?

Login once in a setup project and share the state across workers.

TYPESCRIPT
// auth.setup.ts: runs once before all workers
import { test as setup } from '@playwright/test';

setup('Authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Username').fill(process.env.TEST_USER!);
  await page.getByLabel('Password').fill(process.env.TEST_PASS!);
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.context().storageState({ path: '.auth/user.json' });
});

// All workers load this state: no parallel login storms

Why is "more workers" counterproductive past the bottleneck?

Speedup holds only while the constraining resource has headroom. Past that point, extra workers queue.

DIAGRAM
  Wall-clock time vs worker count:

  Time │
       │  ●
       │    ●
       │      ●
       │        ●           Sweet spot
       │          ● ← ← ← ←
       │            ●
       │              ●  ← Past here: time INCREASES
       │                ●
       │                  ●  Contention, timeouts
       └────────────────────────
         2  4  6  8  10  12  Workers

  At 6 workers: environment has headroom, time improves
  At 10+ workers: DB pool saturated, tests timeout
  Total time actually increases due to contention

How do you find the right worker count?

Measure rather than guess. Start near the machine's core count and watch wall-clock time, flakiness, and environment metrics.

BASH
# Sweep to find the sweet spot
for w in 2 4 6 8 10 12; do
  echo "Workers: $w"
  time npx playwright test --workers=$w 2>&1 | tail -1
done
DIAGRAM
  Workers │ Time    │ Flaky Rate │ DB CPU
  ────────┼─────────┼────────────┼───────
  2       │ 12m 30s │ 0%         │ 15%
  4       │ 6m 45s  │ 0%         │ 30%
  6       │ 4m 50s  │ 0.5%       │ 55%
  8       │ 4m 10s  │ 1%         │ 75%
  10      │ 4m 30s  │ 5%         │ 95%
  12      │ 5m 15s  │ 12%        │ 100%

  Sweet spot: 8 workers (best time with acceptable flakiness)

How do you handle tests that must run serially?

Use test.describe.serial for genuinely order-dependent tests.

TYPESCRIPT
test.describe.serial('Loan lifecycle (must be ordered)', () => {
  test('Apply for loan', async ({ page }) => { /* ... */ });
  test('Approve loan', async ({ page }) => { /* ... */ });
  test('Disburse loan', async ({ page }) => { /* ... */ });
  test('Close loan', async ({ page }) => { /* ... */ });
});

What are common interview follow-up questions?

  • "What is the default worker count?" Half the number of CPU cores, or 1 in CI unless configured.
  • "Can two workers share a browser?" No. Each worker has its own browser process.
  • "How do you debug a parallel failure?" Run with --workers=1 to eliminate parallelism, then check if the test depends on data or order from another test.

Visual Overview

DIAGRAM
  Worker-Based Parallel Execution
  ────────────────────────────────

  Test Runner
  ┌──────────────────────────────────┐
  │ Shards test files across workers │
  └──────────┬───────────────────────┘
             │
    ┌────────┼────────┬────────┐
    │        │        │        │
    ▼        ▼        ▼        ▼
  Worker 1  Worker 2  Worker 3  Worker 4
  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
  │ Own  │  │ Own  │  │ Own  │  │ Own  │
  │ proc │  │ proc │  │ proc │  │ proc │
  │ Own  │  │ Own  │  │ Own  │  │ Own  │
  │browser│ │browser│ │browser│ │browser│
  │ Own  │  │ Own  │  │ Own  │  │ Own  │
  │ data │  │ data │  │ data │  │ data │
  └──────┘  └──────┘  └──────┘  └──────┘
     │         │         │         │
     └─────────┴─────────┴─────────┘
                    │
              Shared environment
              (the bottleneck)

Key Takeaway

Playwright runs tests in parallel with a worker-based model: each worker is an isolated process with its own browser, and test files are sharded across workers. I set the worker count in config and enable fullyParallel to also parallelize within files.

Choosing the right level is a trade-off. Tests must be independent, data must be isolated per worker with unique keys, authentication state must be reused so workers start cheaply, and the environment and runner machine must have capacity. More workers is counterproductive past the environment's bottleneck: extra workers queue on the constrained resource, causing contention, timeouts, and increased flakiness.

I find the sweet spot by measuring: starting near the core count, sweeping worker counts, and watching wall-clock time, flakiness rate, and environment metrics. I settle where time stops improving and flakiness starts rising.

At the National Bank of India someone had set workers very high, but the suite was slower and flaky because the database connection pool was saturated. I measured the sweep, found the sweet spot, dropped workers to that level, isolated data with unique keys, and reused auth state. The suite ran faster and stopped flaking.

Remember one sentence: parallelize with isolated workers and unique data per test, but measure the sweet spot because past the environment's capacity, more workers increase contention and flakiness rather than speed.

---

Share
Use ← → arrow keys to navigate