Home / Blog / Playwright workers and shards

Playwright workers and shards: which one your suite is short of

Quick answer

Workers are parallel processes on one machine. Shards are slices of the test list handed to different machines. They multiply: four shards of four workers is sixteen tests at once. If your runner sits idle, raise workers. If it is pinned and still slow, add shards. If neither helps, the suite is slow, and splitting it spreads the bill.

npx playwright test starts a set of operating-system processes and hands each of them tests. The runner orchestrates them, they cannot talk to one another, and every one of them launches its own browser. One machine, one command, several processes: those are the workers, and you choose how many with workers in playwright.config.ts or with -j on the command line.

npx playwright test --shard=2/4 is a different mechanism. It runs a quarter of the test list and exits. Playwright does not start the other three; something outside it does, and each of those is a separate playwright test run with its own config load, its own browser launch, its own workers and its own report.

A shard is a whole test run that has been told to skip most of the tests. Everything expensive about starting a run happens once per shard, which is why the shard-count section below is arithmetic. The samples and the timings on this page were both produced on Playwright 1.62.

Workers or shards: which one is my suite short of?

Start the suite on the machine that runs it and watch the processor while it works. What you see settles the question before you touch any configuration.

The two compose. Four shards each running four workers is sixteen tests in flight, which means sixteen browsers against one staging environment at the same moment, and a staging database has an opinion about that. Browser projects multiply the list again, since a suite configured for Chromium, Firefox and WebKit has three times the tests to distribute — and Playwright's WebKit build is an engine rather than Safari on somebody's iPhone, so a third project buys coverage of the engine and not of the device.

How many workers should I use?

The default lives on TestConfig.workers, and the parallelism guide does not carry it at all: "Defaults to half of the number of logical CPU cores." The command-line reference states the same default in the other notation, as 50%, because the option accepts a percentage string as well as a number.

Write the percentage. workers: '50%' still means half the machine after somebody moves the job to a larger runner, where a hard-coded 4 is contention on a two-core box and idle silicon on a sixteen-core one.

On CI the documentation asks for a single worker, and it names stability and reproducibility as the reasons: one test at a time, with the whole machine to itself. That is a real recommendation and plenty of green suites depend on it. It also has a price, which the same section names in its last sentence when it points at sharding — with one worker the run costs the whole serial length of the suite, and the sharding it sends you to is what buys the time back. A team that takes the first sentence and skips the fourth has serialised its pipeline and has not connected that line to how long the pipeline now takes.

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  workers: process.env.CI ? '50%' : undefined,
  reporter: process.env.CI ? 'blob' : 'html',
  use: { channel: 'chrome' },
});
Playwright 1.62 · TypeScript

Every line in there is a decision: tests balanced test by test across shards, a worker count expressed as a share of whatever machine it lands on, and a reporter whose output a later job can merge into one.

Every worker carries a browser, so the curve bends and then flattens, and the only way to know where is to run it. Here is one machine's answer: 192 tests in 24 files, each test loading a page, filling three fields, clicking a button and asserting on the result, against an application served from the same laptop. Four physical cores, eight logical, 32 GB, Windows 11, Chrome through channel: 'chrome', fullyParallel: true. Three passes at every setting. The figures are the runner's own reported durations, which is the quantity the next section calls T.

WorkersFastest of threeSlowest of threeSpeed-up on the fastest
195.5s129.0s1.00×
258.4s80.1s1.64×
356.1s79.3s1.70×
452.1s68.6s1.83×
554.4s63.2s1.76×
654.1s64.7s1.76×
851.8s56.3s1.84×

Most of the gain arrives with the second worker and it is 1.64 times, not two. After four, the differences are inside the run-to-run spread: going from four workers to eight took 0.3 of a second off a run that moved by seconds between passes. Eight logical cores bought nothing like eight times. Four is this machine's physical core count and half its logical count, which is where the documented default lands, and on this suite that is also where more processes stopped being worth starting.

Your suite will bend somewhere else. One that spends its time waiting on a remote staging environment scales further than one that spends it rendering, because waiting costs no core. So take the shape from this and run the experiment on the runner, not on a laptop: raise the number until the wall clock stops falling, step back one, and repeat it when the runner size changes.

How does sharding actually work?

--shard=x/y is one-based. --shard=1/4 is the first quarter and --shard=4/4 is the last; there is no 0/4. The configuration form is shard: { total: 5, current: 2 } and counts from one as well.

Each of those commands is a complete startup. Config load, fixtures, whatever setup the configuration declares, browser launch — and then a slice of the tests. Shards do not coordinate, do not share a cache and do not know how many tests the others got.

Both kinds of setup run in every shard. Run on 1.62.1: a globalSetup and a setup project, each appending its own process id to a file, across --shard=1/2 and --shard=2/2. Both wrote a line under both shards, with a different process id each time, and each shard reported five passed — its four tests plus the setup project's one. A setup project's test is added to every shard rather than divided between them: it is a per-shard constant.

How the slice is chosen decides most disappointing shard runs. With fullyParallel: true Playwright splits at the level of individual tests, so each shard gets a comparable number of them. Without it the unit is the file, whole files land on one shard, and unevenly sized files wreck the distribution: one 200-test file beside nineteen 3-test files puts more tests on a single shard than the other nineteen carry between them. Playwright's sharding guide says as much under its own heading on balancing, and we ran it rather than taking it: eight tests in one file across two shards put all eight on the first shard and left the second with globalSetup and nothing else, and adding fullyParallel: true to the same eight tests split them four and four. In the limit a suite that lives in one file cannot be sharded at all — the extra machines start, pay the fixed cost, and run nothing.

Something outside Playwright has to launch the other shards. In practice that is one CI job each, and on GitHub Actions that is a matrix that turns four shards into four jobs.

How many shards should I run?

Two measurements decide it, and both of them live on your pipeline.

T is the test time. How long the suite takes when one job runs all of it, timed from the first test to the last. This is the quantity that divides.

F is the fixed cost of a job. Checkout, npm ci, the browser install or a pull of the official Playwright image, config load, globalSetup and every setup project — all of which the run above confirms are paid once per shard — and the artifact upload at the end. Read it off a job that contains one trivial test. This is the quantity that multiplies, and it is the one that usually goes untimed. A setup project that signs in and saves storage state is often the largest term in it, and it is the term people leave out of F because it looks like part of the suite.

With N shards the wall clock is about F + T/N, because the tests divide and the fixed part does not. The bill is about N × F + T, because each shard pays F over again. One number falls and the other rises.

Going from N shards to N+1 saves T/N − T/(N+1), which is T divided by N × (N+1), and it costs a further F. Set those equal and you get N × (N+1) = T/F, so the next shard earns nothing at roughly N = √(T/F). Two lines, two inputs, and both inputs are yours — redo the arithmetic on your own figures before you trust the square root.

MeasureHowYour number
T — test timeOne job, whole suite, first test to last
F — fixed costOne job, one trivial test, whole job time
T / FDivide
√(T / F)The point where the next shard earns nothing

Four things cap N below that crossover. Wall clock never drops under F plus the duration of the slowest single test, because a test cannot be split. Without fullyParallel it never drops under F plus the slowest file. Shards beyond the concurrency your CI plan allows sit in a queue rather than running, and a queue is time your team waits. And the merge job at the end is another job with its own F.

Getting T and F out of a pipeline nobody on the current team wrote is often the hard part, and it is the sort of work we take on when a client hands us their Playwright pipeline.

Why do my tests fail only when they run in parallel?

Parallelism does not create these failures. It removes the accident of ordering that was hiding them, and a suite that has only ever run in one order has never been tested for order dependence. What surfaces on the afternoon you turn it on is usually one of these.

The second of those has a documented answer: one account per parallel worker, signed in once in a worker-scoped fixture, with every test that worker runs sharing the session. It needs one seeded account per worker, and somebody has to own creating them. The four session patterns and what each one costs works that fixture through.

Both indices are on testInfo, and eight lines settle what they are on your version before you index anything with one.

import { test } from '@playwright/test';

for (const n of [1, 2, 3, 4, 5, 6, 7, 8]) {
  test(`case ${n}`, async ({}, testInfo) => {
    console.log(`case ${n}: workerIndex=${testInfo.workerIndex} parallelIndex=${testInfo.parallelIndex}`);
  });
}
Playwright 1.62 · TypeScript · passes

Run it with npx playwright test indices --workers=4 --fully-parallel and the numbering answers for itself. Three runs on 1.62.1 gave the same range every time, and the tests land wherever a worker comes free:

case 2: workerIndex=1 parallelIndex=1
case 1: workerIndex=0 parallelIndex=0
case 5: workerIndex=1 parallelIndex=1
case 6: workerIndex=0 parallelIndex=0
case 8: workerIndex=1 parallelIndex=1
case 7: workerIndex=0 parallelIndex=0
case 3: workerIndex=2 parallelIndex=2
case 4: workerIndex=3 parallelIndex=3
  8 passed (2.6s)

Both indices count from zero here: four workers give 0 through 3, so a four-element pool is indexed directly and a guess that the numbering starts at one crashes on the fourth worker. Check it on the version you are running; the numbering is cheap to confirm and expensive to assume. A restart is why the pool should be keyed on the parallel index: run three tests through one worker and fail the first, and the replacement arrives with worker index 1 and parallel index still 0, so it picks up the account the dead worker held instead of asking for a fifth. The fixtures article takes the two indices and the cost of a discarded worker further.

What about tests that genuinely have to run in order?

Out of the box, test files run in parallel and the tests inside one file run in declaration order in a single worker. Most readers have that setting and have never thought about it.

fullyParallel: true, or --fully-parallel on the command line where the documented default is false, makes every test anywhere a candidate for any worker. It is what test-level shard balancing needs, and it retires any assumption that the second test in a file runs after the first.

test.describe.serial is the escape hatch, and it has two costs the documentation states and readers skip. If one test in the group fails, the rest are skipped. The whole group retries together. Playwright's own position is that serial is not recommended and that isolated tests that can run independently are better.

import { test, expect, type Page } from '@playwright/test';

test.describe.configure({ mode: 'serial' });

let page: Page;

test.beforeAll(async ({ browser }) => {
  page = await browser.newPage({ baseURL: test.info().project.use.baseURL });
});

test.afterAll(async () => {
  await page.close();
});

test('sets the quantity', async () => {
  await page.goto('/');
  await page.getByLabel('Qty').fill('3');
});

test('pays for what the first test put in the basket', async () => {
  await page.getByRole('button', { name: 'Pay' }).click();
  await expect(page.locator('#status')).toHaveText('paid 3');
});
Playwright 1.62 · TypeScript · passes

Break the first of those two on 1.62 and the summary reads 1 failed and 1 did not run. The second test is never attempted, so a failure early in a serial group hides whatever the rest of the group would have told you. The counterpart runs the other way: test.describe.configure({ mode: 'parallel' }) lets the tests inside one file run concurrently, and mode: 'default' opts one describe block back out when fullyParallel is on globally.

Serial mode is a debt instrument. It buys a green build today and charges interest as a group that cannot be spread across shards, cannot be retried a test at a time, and grows slower every time somebody adds a step. Occasionally it is the right purchase. A multi-step checkout that genuinely cannot be re-entered halfway is one. Know that you have bought it.

Where did my test report go?

Four shards produce four partial reports and no single answer to whether the build passed. The blob reporter is the mechanism: it writes one report per shard into blob-report/, with the shard number in the file name so they do not collide, and npx playwright merge-reports --reporter html ./all-blob-reports turns a directory of them into one HTML report. That command is one more job with an F of its own. Choosing a reporter and reading what it produces is a longer subject than this section.

Sharding didn't make it faster

Sharding hides a slow suite; it does not repair one. A forty-minute suite that takes forty minutes because every test signs in through the user interface is still forty minutes of signing in through the user interface after you split it four ways. You have bought ten-minute feedback and quadrupled the runner bill, and you have made the cause harder to see, because nobody profiles a suite that comes back in ten minutes.

Measure before you multiply. Every test's duration is already sitting in the report you have, and if ten specs account for half the run, four machines is an expensive answer to a question one afternoon of profiling answers better. --last-failed re-runs only what failed last time and --repeat-each runs each test N times, which is most of the toolkit that afternoon needs.

Questions

What is the difference between workers and shards in Playwright?

Workers are parallel processes on one machine: a single playwright test command starts several operating-system processes, gives each one its own browser, and hands them tests. Shards are slices of the test list, one per machine: --shard=2/4 runs a quarter of the tests and exits, and something outside Playwright has to run the other three as separate full runs. The two compose, so four shards of four workers is sixteen tests in flight. Raise workers while the machine is idle; add shards once it is pinned and the run is still long.

How many workers should I use?

The documented default is half of the number of logical CPU cores, and the option accepts a percentage string as well as a number, so workers: '50%' writes that default down in a form that survives a move to a bigger runner. The CI documentation asks for a single worker there, naming stability and reproducibility as the reasons, on the grounds that one test at a time has the whole machine to itself. That is a trade with a bill attached: one worker costs you the whole serial length of the suite. On your own hardware, raise the number until the wall clock stops falling and then step back one.

How many shards should I run?

Two measurements decide it. T is how long the suite takes when one job runs all of it, timed from the first test to the last. F is what a job spends on everything that is not tests, which you read off a job containing one trivial test. Wall clock with N shards is about F + T/N and billed time is about N x F + T, so another shard earns nothing once N reaches roughly the square root of T divided by F. Measure F before you pick a number, because a large F relative to T means you can afford very few shards.

Why do my Playwright tests fail when I run them in parallel?

Because they were order-dependent already and running in order was hiding it. The usual causes are two tests editing the same record, a login that invalidates the sessions other tests are holding, two tests writing to the same file path, and a variable in module scope that one worker shares and another does not. The fixes rhyme: have each test create the data it asserts on, give every parallel worker its own account from a worker-scoped fixture, and derive any shared path from the worker index.

Does sharding make my tests cheaper?

No. It makes them finish sooner and it costs more. Wall clock falls towards F + T/N, but each shard pays the fixed cost of a job over again, so billed minutes climb by roughly N x F. Split a suite three ways and you have three checkouts, three dependency installs and three browser downloads for the same tests, plus a merge job at the end. What you are buying is feedback time, which is usually worth buying. The invoice still goes up, and it is better to say so before the person who signs it notices.

Send us T and F

The serial test time of your suite, and the time one job spends on everything that is not tests. With those two we can say where another shard stops earning its keep, and whether the problem was a worker count all along. If the pipeline was inherited and nobody can get at either number yet, say that instead.