Definition
- 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
workersin config or CLI. EnablefullyParallelto 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.
// 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
});
# 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.
// 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.
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.
// 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.
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.
# 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
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.
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=1to eliminate parallelism, then check if the test depends on data or order from another test.
Visual Overview
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.
---