Parallelize Playwright E2E with Shards and Workers
Split E2E specs across CI machines and in-process workers, isolate each workers browser context, and keep total runtime under a budget.
Task
Cut an E2E suite wall-clock time by sharding across machines and workers without introducing cross-test interference.
When to use this
The Playwright suite grew to 40 minutes and now runs only nightly. Feedback arrived too late to be useful, so regressions shipped between runs.
Prerequisites
- Playwright with specs structured per feature file
- CI able to run a matrix of parallel jobs
- A staging environment tolerant of concurrent browser traffic
Steps
1. Enable in-process workers first.
export default defineConfig({
testDir: "./e2e",
workers: process.env.CI ? 4 : 2,
fullyParallel: true,
use: { trace: "on-first-retry" },
});
Expected output: Spec files run concurrently in isolated browser contexts on one machine; most suites already need nothing more.
2. Shard across CI machines for the next factor.
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
Expected output: Four CI machines each run a quarter of the specs, on top of per-machine workers.
3. Isolate state per test to keep parallelism safe.
test("checkout flow", async ({ page, request }) => {
const user = await createUserViaApi(request); // fresh user per test
await page.goto(`/login?token=${user.autoLoginToken}`);
});
Expected output: Each test owns its data, so concurrent execution cannot collide on shared fixtures.
4. Budget the total and rebalance shards.
npx playwright test --shard=1/4 --reporter=json 2>/dev/null | jq '.stats.duration'Expected output: Per-shard durations within a few seconds of each other confirm no straggler defines the runtime.
5. Keep traces only for failures.
use: { trace: "on-first-retry" },
retries: 1,
Expected output: Artifact storage stays affordable at parallel scale, while failed tests still ship full traces for diagnosis.
Troubleshooting
Problem
Tests fail only when parallel
Solution
They share a user or a singleton resource; generate per-test entities through the API and delete the shared fixture assumption.
Problem
Staging saturates under concurrent browsers
Solution
Cap total concurrency: workers times shards must fit what the environment serves; raise shards and lower workers, or scale the environment.
Problem
One shard always runs longest
Solution
Rebalance by duration: run once with reporting, split heavy feature files across shards instead of alphabetically.
Limitations
- Parallelism multiplies load on the target environment; budget its capacity before scaling shards.
- Video and trace artifacts multiply storage; keep them failure-scoped to stay sustainable.
Two axes of parallelism
Playwright parallelizes along two axes: workers run spec files concurrently inside one machine, and shards split the suite across machines. Most long suites need both. The prerequisite is the same for each: tests must own their state, because any fixture shared between tests becomes a race the moment anything runs concurrently.
export default defineConfig({
workers: process.env.CI ? 4 : 2,
fullyParallel: true,
use: { trace: "on-first-retry" },
});
// CI matrix
npx playwright test --shard=${{ matrix.shard }}/4
Statelessness is the enabling constraint
A test that logs in as demo user cannot run in parallel with itself, but a test that provisions its own user through the API can run beside unlimited siblings. Building per-test setup into the suite is what converts parallelism from a risky configuration into a boring speedup, and it also removes the order-dependence that causes rerun flakiness.
Concurrency is a budget, not a maximum
Workers times shards is the simultaneous browser count your staging serves. Going past it does not slow results linearly, it introduces timeouts and failures that look like bugs in the product. Decide the number from environment capacity, then split it across shards and workers however scheduling is cheapest.