By default: test files run in parallel across workers, tests within one file run sequentially. That's almost always what you want. I set workers: 2 on CI for predictability, and use workerIndex to isolate test data between parallel workers.
How workers work
Each worker is an isolated OS process; files run sequentially inside a worker, workers run in parallel
Each worker is a separate OS process — its own Node.js instance, its own browser, no shared memory with other workers. Workers can't talk to each other. Playwright reuses a worker for multiple test files to avoid the browser startup cost — when one file finishes, the next file runs in the same worker.
After a test failure, the worker is always killed and a fresh one starts — guaranteeing no stale state bleeds into subsequent tests.
By default Playwright uses half the CPU cores. On CI I pin workers to a fixed number — too many workers on a shared runner causes resource contention and flaky tests. Locally I let it use defaults.
ts
// playwright.config.tsexportdefault defineConfig({
// На CI — 2 воркери (predictable); локально — дефолт (усі ядра / 2)
workers: process.env.CI ? 2 : undefined,
})
bash
# Перевизначити через CLI
npx playwright test --workers 4# Запустити без паралелізму (для дебагу)
npx playwright test --workers=1
fullyParallel — run tests within a file in parallel too
By default, tests within one file always run sequentially — that's fine for most cases. fullyParallel: true makes every individual test run in its own worker. Useful if you have large files with many independent tests. The downside: shared state (like let orderId between tests) breaks because each test runs in a separate process.
ts
// Глобально — кожен тест у своєму воркеріexportdefault defineConfig({
fullyParallel: true,
})
// Якщо fullyParallel увімкнено глобально — можна відключити для одного describetest.describe('checkout flow', () => {
test.describe.configure({ mode: 'default' }) // Sequential навіть при fullyParalleltest('step 1: add to cart', async ({ page }) => { /* ... */ })
test('step 2: enter shipping', async ({ page }) => { /* ... */ })
})
workerIndex — isolate test data between workers
When multiple workers run at the same time, they can't share a test user — they'd overwrite each other's data. I use workerIndex to give each worker its own user. Each worker gets a unique index (starting at 0), and I create a separate user per index.
ts
// fixtures.tsimport { test as base } from'@playwright/test'exportconsttest = base.extend({
// Worker-scoped: один раз на воркер, не на кожен тест
testUser: [async ({}, use, workerInfo) => {
const userName = `worker-${workerInfo.workerIndex}@example.com`// Створити юзера для цього воркераawait createUser(userName)
await use(userName)
// Прибрати після всіх тестів воркераawait deleteUser(userName)
}, { scope: 'worker' }],
})
ts
// test-worker-index через process.env (якщо не використовуєш фікстуру)test('create order', async ({ page }, testInfo) => {
const workerIdx = testInfo.workerIndex
await page.goto(`/login?user=worker-${workerIdx}`)
// ... тест використовує ізольованого юзера
})
maxFailures — stop early when things break
If 50 tests fail in the first minute, there's no point running the other 950. maxFailures stops the whole run after hitting the limit — saves CI time when there's a fundamental problem.
ts
// playwright.config.tsexportdefault defineConfig({
// На CI: зупинитися після 10 падінь — не витрачати ресурси на зламаний suite
maxFailures: process.env.CI ? 10 : undefined,
})
bash
# Або через CLI
npx playwright test --max-failures=5