Parallel Execution & Sharding
Run thousands of tests faster by executing them in parallel across workers and sharding across CI machines.
How Playwright Parallelises Tests
By default, Playwright Test runs test files in parallel using a pool of worker processes, where each worker has its own browser instance. Tests within a single file run serially by default to preserve shared state. Enabling fullyParallel: true in the config also runs individual tests within a file in parallel, which is safe when each test is fully independent (the recommended design).
Configuring Workers
The workers option controls how many parallel processes run simultaneously. On a developer machine, leaving it as undefined lets Playwright auto-detect (typically half the CPU count). In CI, set it explicitly — for example 4 — to match the available runners. Saturating all CPU cores rarely helps because browser tests are I/O-bound, not CPU-bound.
Sharding Across CI Machines
Sharding splits the test suite across multiple CI machines or jobs. Pass --shard=1/3 to run the first third of tests, --shard=2/3 for the second, and so on. After all shards complete, merge the blob reports with npx playwright merge-reports to get a single unified HTML report. Pytest users achieve equivalent parallelism with pytest-xdist and the -n auto flag.
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Run every test in every file in parallel
fullyParallel: true,
// 4 workers in CI, auto in local dev
workers: process.env.CI ? 4 : undefined,
// Merge-able blob reports for sharding
reporter: process.env.CI
? [[ 'blob' ], [ 'github' ]]
: [[ 'html', { open: 'on-failure' } ]],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
});
/* Run commands:
Local: npx playwright test
Shard 1: npx playwright test --shard=1/3
Shard 2: npx playwright test --shard=2/3
Shard 3: npx playwright test --shard=3/3
Merge: npx playwright merge-reports --reporter html ./all-blob-reports
*/
import { test, expect } from '@playwright/test';
// These tests are safe to run in parallel because
// each one creates and deletes its own isolated data
test('create order A', async ({ request, page }) => {
const { id } = await (await request.post('/api/orders', {
data: { product: 'Widget A' }
})).json();
await page.goto(`/orders/${id}`);
await expect(page.getByText('Widget A')).toBeVisible();
await request.delete(`/api/orders/${id}`);
});
test('create order B', async ({ request, page }) => {
const { id } = await (await request.post('/api/orders', {
data: { product: 'Widget B' }
})).json();
await page.goto(`/orders/${id}`);
await expect(page.getByText('Widget B')).toBeVisible();
await request.delete(`/api/orders/${id}`);
});
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.*;
// Enable parallel execution in junit-platform.properties:
// junit.jupiter.execution.parallel.enabled=true
// junit.jupiter.execution.parallel.mode.default=concurrent
@Execution(ExecutionMode.CONCURRENT)
class ParallelTest extends BaseTest {
@Test
void orderA() {
page.navigate("/orders/create");
page.getByLabel("Product").fill("Widget A");
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Place Order")).click();
}
@Test
void orderB() {
page.navigate("/orders/create");
page.getByLabel("Product").fill("Widget B");
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Place Order")).click();
}
}
# pytest.ini — enable xdist parallel execution
[pytest]
addopts = -n auto --dist=loadscope
# Install: pip install pytest-xdist
# Run: pytest -n 4 (4 workers)
# Run: pytest -n auto (auto-detect CPUs)
import pytest
from playwright.sync_api import APIRequestContext, Page, expect
@pytest.mark.parametrize("product", ["Widget A", "Widget B", "Widget C"])
def test_create_order(
product: str,
api_context: APIRequestContext,
page: Page
):
# Each parametrized test is isolated by its own order
resp = api_context.post("/api/orders", data={"product": product})
order_id = resp.json()["id"]
page.goto(f"/orders/{order_id}")
expect(page.get_by_text(product)).to_be_visible()
api_context.delete(f"/api/orders/{order_id}")
Written by PV
© 2026 All Rights Reserved