World’s largest virtual agentic engineering & quality conference
Playwright parallel testing explained with measured numbers: workers vs sharding, a 1 to 12 worker benchmark, why speedup stalls, and how to shard across CI.

Jaydeep Karale
Author
Srinivasan Sekar
Reviewer
Last Updated on: August 6, 2026
Doubling your Playwright workers does not halve the run time, and past a certain count it stops helping at all. The numbers below come from running the same suite at every worker count from 1 to 12 on one machine.
Most guides to this topic show the configuration and stop. Everything below the configuration sections comes from running the same 24-test suite on one machine at worker counts from 1 to 12, and then again split into four shards. The headline result is that on an 8-core machine, 8 workers finished 5.2 times faster than 1 worker rather than 8 times, and going to 12 workers bought half a second.
Overview
Playwright parallel testing runs several tests at once in separate worker processes, each holding its own browser and an isolated context. Two independent settings control it: workers add concurrency on one machine, and shards spread the test list across several machines. They multiply rather than compete.
What Do the Settings Actually Buy?
What Happens Past the Local Ceiling?
Worker scaling stops paying at the core count and shard scaling means provisioning machines that idle between runs. Running the same suite against cloud infrastructure such as TestMu AI removes the core count from the equation, though a shared login account or a long serial block scales just as badly there.
Playwright parallel testing means running several tests at the same time in separate worker processes, each with its own browser and its own isolated context. It is the main lever available for cutting suite runtime, and it is controlled by two independent settings that get confused constantly: workers, which adds concurrency on one machine, and shards, which spread the test list across several machines.
Playwright runs tests in worker processes. Each worker is a separate operating-system process that owns a browser instance, and each test inside that worker gets a fresh browser context, which is an isolated profile with its own cookies, storage, and cache. Two tests running at the same time cannot see each other's browser state.
What varies is the unit being distributed. Out of the box Playwright parallelises across test files: a file is assigned to a worker, and the tests inside it run in declaration order on that one worker. Setting fullyParallel changes the unit from the file to the individual test, so any free worker can pick up any test.
That distinction decides whether the worker count does anything at all. A suite of three large spec files cannot use more than three workers under the default behaviour, however high you set the number. This is the most common reason a team raises the worker count, sees no improvement, and concludes that parallelism does not help their suite.
Parallel execution is not unique to Playwright, and the general tradeoffs are covered in what is parallel testing. What is specific here is that Playwright gives you two dials rather than one, and that they are frequently set as though they were the same dial.
Workers and shards operate at different levels and multiply rather than compete. Four machines running two workers each gives eight tests in flight, exactly as one machine with eight workers does, but the failure modes and the cost are not the same.
| Consideration | Workers | Shards |
|---|---|---|
| What it splits | Tests across processes on one machine. | The test list across separate machines or CI jobs. |
| Set with | The workers option in the config, or the workers flag. | The shard flag, passing an index and a total. |
| Hard ceiling | The CPU and memory of one machine. | The number of CI jobs you are willing to pay for. |
| Cost of adding one | Nothing, until the machine runs out of cores. | A whole additional machine for the length of the run. |
| Reporting | One report, produced normally. | One partial report per shard, which must be merged afterwards. |
| Typical failure | Timeouts under CPU contention, which read as flaky tests. | Uneven shards, where one slow shard sets the wall clock for all of them. |
| Reach for it when | The machine still has idle cores. | The machine is saturated and the run is still too slow. |
The order matters. Saturating the machine you already pay for is free; adding machines is not. Teams that shard before measuring their worker curve routinely pay for four CI runners to do what six workers on one runner would have done.
Three settings cover almost every case. This is the configuration used for every number reported further down.
// playwright.config.js
module.exports = {
testDir: './tests',
// Distribute individual tests rather than whole files.
// Without this, a suite of 3 spec files never uses more than 3 workers.
fullyParallel: true,
// Leave unset to accept the default: roughly half the available cores
// locally, and half the reported cores in CI. Set it explicitly only
// after measuring, and read it from the environment so CI can differ.
workers: process.env.CI ? 4 : undefined,
// Blob reports are the ones that can be merged after a sharded run.
reporter: process.env.CI ? [['blob']] : [['list']],
use: { headless: true },
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
};Both dials can also be set per run, which is how you measure without editing the config between attempts:
# Change the worker count for one run
npx playwright test --workers=4
# Run one quarter of the tests, as CI job 2 of 4
npx playwright test --shard=2/4 --workers=2
# Merge the blob reports the shards produced
npx playwright merge-reports --reporter=html ./all-blob-reportsPlaywright prints the worker count it settled on as the first line of every run, which is the fastest way to confirm that the setting took effect rather than being overridden somewhere:
Running 24 tests using 4 workers
✓ 2 [chromium] › tests/bench.spec.js:8:3 › case 2 (1.4s)
✓ 1 [chromium] › tests/bench.spec.js:8:3 › case 4 (1.4s)
✓ 4 [chromium] › tests/bench.spec.js:8:3 › case 3 (1.4s)
✓ 3 [chromium] › tests/bench.spec.js:8:3 › case 1 (1.4s)
✓ 6 [chromium] › tests/bench.spec.js:8:3 › case 6 (1.2s)
24 passed (8.1s)The number before each test name is the worker index. Note that the tests do not complete in declaration order, which is the visible sign that distribution is actually happening.
Method first, so the Playwright parallel testing numbers can be judged. The suite is 24 tests, each performing a page load, three interactions, two assertions, and a fixed one-second wait, which puts every test at roughly 1.2 to 1.4 seconds of work. The page under test is served from the local filesystem rather than over the network, deliberately: the goal is to measure how Playwright scales across workers, and network variance would bury that signal.
The machine is an Apple M3 with 8 CPU cores and 16 GB of memory, running macOS 26.5 and Playwright 1.62.1 against headless Chromium, with fullyParallel enabled. Each worker count was run twice, cold, with nothing else competing for the machine.
Every figure on this page comes from that setup, and the setup is stated so you can disagree with it. Published parallel-testing numbers are usually a customer anecdote with the suite size, the hardware, and the run count left out, which makes them impossible to check and impossible to compare against your own. The suite here is deliberately uniform, at roughly 1.2 to 1.4 seconds per test, because a suite with a wide spread of test durations measures the spread rather than the parallelism.
| Workers | Run 1 | Run 2 | Speedup vs 1 worker | Efficiency per worker |
|---|---|---|---|---|
| 1 | 28.4s | 27.9s | 1.0x | 100% |
| 2 | 14.7s | 14.5s | 1.9x | 97% |
| 4 | 9.4s | 8.2s | 3.2x | 80% |
| 6 | 6.6s | 6.1s | 4.4x | 74% |
| 8 | 5.5s | 5.4s | 5.2x | 65% |
| 12 | 5.1s | 4.9s | 5.6x | 47% |
Three things in that table are worth acting on.
The first jump is nearly free. Going from 1 to 2 workers returned 97 percent of the theoretical doubling, which means a suite still running serially is leaving the largest single improvement on the table. If you change one setting today, change it from 1 to at least 2.
Efficiency then decays steadily rather than falling off a cliff. At 8 workers on 8 cores the suite ran 5.2 times faster, not 8 times. The missing 35 percent is browser startup paid once per worker, plus contention for CPU, memory bandwidth, and disk. That gap is normal, and a suite hitting it is correctly configured rather than broken.
Past the core count the curve flattens. Twelve workers on eight cores improved on eight workers by roughly half a second, a 9 percent gain for 50 percent more processes, each holding its own browser in memory. On a machine with less headroom that overcommitment is where runs start timing out under load, which surfaces as flakiness rather than as slowness and gets misdiagnosed accordingly.
Your absolute numbers will differ, because they depend on how much of each test is spent waiting on a network the CPU cannot help with. The shape of the curve is what transfers: strong early returns, a steady decay, and a flat section beyond the core count that is not worth paying for.
Note: Worker counts are capped by the machine you own. TestMu AI runs Playwright suites across 3,000+ browser and OS combinations in parallel, so concurrency stops being a function of local cores. Start free
Playwright isolates the browser. It does not, and cannot, isolate anything on the other side of it. Every test that fails only under parallel execution is failing on state that lives outside the browser context, and the categories are predictable.
| Shared thing | How it fails under parallel execution | Fix |
|---|---|---|
| One login account | A second worker signs in and invalidates the first worker's session mid-test. | One account per worker, keyed on the worker index, seeded before the run. |
| A shared database row | One test edits the record another test is asserting on, producing failures that move between tests. | Each test creates the data it needs and cleans up only what it created. |
| Fixed filenames or IDs | Two workers upload report.csv at once and the second overwrites the first. | Suffix every generated name with the worker index or a random token. |
| Global application state | A feature flag or setting toggled by one test changes behaviour for everything running. | Scope the setting to the test account, or isolate those tests in a serial block. |
| A fixed port or service | Two workers bind the same port and the second fails to start. | Derive the port from the worker index, or start the service once globally. |
| Test ordering assumptions | A test that depended on an earlier test's leftovers now runs first, or on another machine. | Make every test set up its own preconditions. Ordering is not a fixture. |
Playwright exposes the worker index for exactly this purpose, and reading it is usually the whole fix:
const { test, expect } = require('@playwright/test');
test('each worker uses its own account', async ({ page }, testInfo) => {
// parallelIndex is stable for the life of the worker and unique across
// the running workers, so account N belongs to worker N for this run.
const user = `qa-user-${testInfo.parallelIndex}@example.com`;
// Suffix anything you write, so two workers never collide on a name.
const uploadName = `report-${testInfo.parallelIndex}-${Date.now()}.csv`;
await page.goto('/login');
await page.locator('#email').fill(user);
// ...
expect(uploadName).toContain(String(testInfo.parallelIndex));
});There is a diagnostic worth knowing. If a suite passes at one worker and fails intermittently at four, the defect is in the tests, in shared data, or in the application's own concurrency handling. Reducing the worker count hides it; it does not remove it, and the third case is a genuine production bug that parallel testing just found for you.
Some sequences genuinely cannot be parallelised, such as a multi-step wizard where each step depends on the previous one having completed. Playwright handles this per describe block rather than forcing the whole suite into one mode.
const { test } = require('@playwright/test');
test.describe('checkout wizard', () => {
// These run on ONE worker, in declaration order. If step 2 fails,
// step 3 is skipped rather than run against a broken state.
test.describe.configure({ mode: 'serial' });
test('step 1: add to cart', async ({ page }) => { /* ... */ });
test('step 2: enter address', async ({ page }) => { /* ... */ });
test('step 3: pay', async ({ page }) => { /* ... */ });
});
test.describe('account settings', () => {
// Independent tests that may all fail without hiding each other.
test.describe.configure({ mode: 'parallel' });
test('change display name', async ({ page }) => { /* ... */ });
test('change timezone', async ({ page }) => { /* ... */ });
});Serial mode has a cost that is easy to miss: the block occupies one worker for its full duration, so a twelve-test serial block in a suite of forty becomes the floor on how fast the whole run can finish, regardless of worker count. Before reaching for it, check whether the dependency is real or whether the second test could simply create its own starting state through the API.
A serial block also changes what a failure means. Because later tests are skipped rather than run, one broken step reports as a single failure plus several skips, which reads very differently in a dashboard. If you rely on Playwright reporting to spot regressions, account for skipped tests in whatever you alert on.
Wall clock is the number teams optimise and machine time is the number they pay for. Those two move in opposite directions once sharding starts, and the benchmark above makes the trade explicit.
| Configuration | Wall clock | Machine time consumed | What the extra spend bought |
|---|---|---|---|
| 1 machine, 1 worker | 28.2s | 28.2s | Baseline. Cheapest possible run, slowest possible feedback. |
| 1 machine, 4 workers | 8.8s | 8.8s | 3.2x faster for no additional spend. This is the free part. |
| 1 machine, 8 workers | 5.3s | 5.3s | Still free, but efficiency is down to 65%. The machine is saturated. |
| 4 machines, 2 workers each | 4.3s | 16.2s | One second, for roughly three times the compute. |
Read the last two rows together. Everything up to the core count is free: the machine is already paid for, and raising the worker count spends nothing. Past that point every further second of wall clock is bought with machine time, and the exchange rate is poor. Our four-shard run spent three times the compute for a 19 percent improvement.
That ratio is why the order of operations matters more than the tooling. Saturating one machine is the highest-return change available and it is the one teams skip, because adding CI runners is a configuration change while measuring a worker curve requires ten minutes of attention.
The exchange rate only justifies itself when the elapsed time is blocking a person or a deploy. A nightly regression run that nobody watches has no reason to be sharded; a pre-merge gate that thirty engineers wait on several times a day has every reason. Decide which one you are optimising before provisioning anything, because the same four runners are excellent value in one case and pure waste in the other.
The benchmark shows where the local approach runs out. Worker scaling stops paying past the core count, and shard scaling means provisioning, warming, and paying for machines that sit idle between runs. Both problems get sharper the moment browser coverage enters, because running the suite on Chromium, Firefox, and WebKit multiplies the test count by three before any sharding decision is made.
The strategy that maps most directly onto Playwright sharding is auto-split on HyperExecute. Instead of you choosing a shard count and maintaining a CI matrix, a discovery command emits the list of test entities and the platform distributes them across the number of concurrent virtual machines you declare, each one an isolated environment where the tests run next to the browsers rather than across a hub-and-node hop. The HyperExecute documentation covers the discovery and concurrency configuration.
The practical difference is what happens when the suite grows. A hand-maintained shard matrix has a number in it that somebody has to remember to raise, and the uneven-shard spread grows quietly until a pipeline that used to take six minutes takes eleven. A declared concurrency against a discovered test list moves that decision out of the YAML matrix.
None of this replaces getting the local numbers right first. A suite with a twelve-test serial block or a shared login account will carry both problems onto any platform, and will scale about as badly there.
Every Playwright parallel testing decision below comes down to one measurement, so run it rather than copying a worker count. It takes about ten minutes and the curve for your suite is the only one that matters:
for w in 1 2 4 6 8; do
echo "workers=$w"
npx playwright test --workers=$w 2>&1 | tail -1
doneSet fullyParallel to true before you start, or the numbers will describe your file layout rather than your worker count. Then read the output for the point where the next doubling stops returning a meaningful improvement, and set the worker count just below it. In our run that point sat at 8 on an 8-core machine; on a 4-core CI container it will sit lower.
Only reach for shards once that number is found and the run is still too slow. When you do, shard on duration rather than count if your reporting lets you: an even split by test count is not an even split by time, and the slowest shard is the only one whose duration you actually pay in wall clock.
If the ceiling turns out to be the machine rather than the configuration, running the same suite against cloud infrastructure removes the core count from the equation, and the Playwright testing documentation covers pointing an existing config at TestMu AI without rewriting the tests. Whichever direction you go, fix the shared-state problems from the table above first, because parallel infrastructure multiplies those failures rather than absorbing them.
Author
Reviewer
Srinivasan Sekar is Director of Engineering at TestMu AI (formerly LambdaTest), where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance