Playwright Sharding
Splitting a browser suite across machines and merging the reports back — the mechanics, the prerequisites, and how to choose a shard count that is actually faster.
2 min read · updated 19 September 2026
Sharding splits a test suite across machines. Four shards of a twenty-minute suite is five minutes, plus overhead.
It is also the fastest way to discover that your suite was never safe to run concurrently — so get parallel execution right first.
#The mechanics
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4Playwright partitions by test file, deterministically, so every shard runs a disjoint subset and the union is the whole suite. Shards need no knowledge of each other.
Workers and shards multiply:
// playwright.config.ts
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : undefined // 4 workers × 4 shards = 16 concurrent
});#In GitHub Actions
jobs:
e2e:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --shard=${{ matrix.shard }}/4
- uses: actions/upload-artifact@v4
if: always()
with:
name: blob-report-${{ matrix.shard }}
path: blob-report/
retention-days: 7
merge:
needs: e2e
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- uses: actions/download-artifact@v4
with: { path: all-blobs, pattern: blob-report-*, merge-multiple: true }
- run: npx playwright merge-reports --reporter=html,github ./all-blobs
- uses: actions/upload-artifact@v4
with: { name: playwright-report, path: playwright-report/ }// The blob reporter is what makes merging possible.
reporter: process.env.CI ? [['blob'], ['github']] : [['html']]Without the merge step you get four partial reports, four flaky counts and four sets of trace links. With it, one report for the whole run — which is what anyone diagnosing a failure actually needs.
#The prerequisites
Sharding multiplies concurrency, so everything that was marginally unsafe becomes reliably broken.
Unique data per shard and per worker.
const email = `buyer-${process.env.SHARD_INDEX ?? 0}-${test.info().workerIndex}-${Date.now()}@example.test`;Ephemeral ports. listen(0), dynamicPort(). A hardcoded port is fine
on one machine and fatal on four.
No shared accounts. Sixteen concurrent tests signing in as
[email protected] and mutating its settings will fail in ways that look
random.
A backend that can take it. Sixteen concurrent browsers is sixteen concurrent users. Connection pools, rate limits and worker counts all have to allow for it — and if they do not, you have found a genuine capacity problem. See load testing.
Per-shard infrastructure. Each shard needs its own application instance and database, or a scoped slice of a shared one.
- run: |
docker compose -p shard-${{ matrix.shard }} up -d --wait
env:
APP_PORT: ${{ 3000 + matrix.shard }}#Choosing a shard count
Each shard pays a fixed setup cost — checkout, npm ci, browser install,
application start. Call it two minutes.
suite 20 min, setup 2 min
1 shard 20 + 2 = 22 min
2 shards 10 + 2 = 12 min
4 shards 5 + 2 = 7 min
8 shards 2.5 + 2 = 4.5 min
16 shards 1.2 + 2 = 3.2 min ← setup now dominates
32 shards 0.6 + 2 = 2.6 min ← 32 machines to save 36 secondsAim for a few minutes of test time per shard. Beyond that you are paying for machines to sit installing browsers.
Caching the browser download is the cheapest way to move that curve:
- uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}#Uneven shards
Playwright distributes by file, so a suite of one enormous file and twenty small ones shards badly — one machine runs for twelve minutes while three finish in two.
Symptoms: wildly different shard durations in the run summary.
Fixes:
- Split the large file. Usually the right answer, and better for readability anyway.
--shardwith durations. Recent Playwright versions can use the previous run's timings; pass--last-failed-style metadata or supply a.last-run.json.fullyParallel: true, so tests within a file also run concurrently — which softens the imbalance considerably.
#Retries with sharding
retries: process.env.CI ? 1 : 0A retry runs on the same shard, so a shard with several flaky tests takes disproportionately longer. That is another reason to treat the flaky count as a defect rather than a cost of doing business — and the merged report is where you can see it for the run as a whole.
#Other runners
The same pattern, different flags:
pytest --splits 4 --group 1 # pytest-split
./gradlew test -Dkotest.shard=1/4 # JUnit 5 via a custom filter, or Gradle test distribution
dotnet test --filter "Shard=1" # usually a custom trait, assigned by a script
npx jest --shard=1/4 # Jest has native sharding tooPlaywright's is the most complete because of the blob reporter and
merge-reports; elsewhere you generally merge JUnit XML and lose some
fidelity.
Common questions
- What is the difference between workers and shards in Playwright?
- Workers are parallel processes on one machine; shards are subsets of the suite run on different machines. They multiply — four shards of four workers is sixteen tests running at once.
- How many shards should I use?
- Enough that each shard runs for a few minutes. Below roughly two minutes per shard the per-machine setup — checkout, install, browser download — dominates, and adding shards makes the wall clock worse rather than better.
- How do I get one report from several shards?
- Use the blob reporter on each shard, upload the blobs as artefacts, then run playwright merge-reports in a job that depends on all of them. Merging is what makes the flaky count and the trace links work across the whole run.
Runnable samples for this page
last test results ↗- TypeScript (Playwright)
browser/tests/ci-cd/playwright-sharding
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Running Tests in ParallelHow parallelism turns hidden coupling into failures, the shared resources that clash, and how to make a suite genuinely safe to run concurrently.
- Testing in GitHub ActionsA complete pipeline — unit tests, integration with containers, sharded Playwright, coverage and artefacts — plus the caching and concurrency settings that make it fast.
- PlaywrightThe default browser automation tool in 2026 — auto-waiting locators, tracing, sharding and fixtures — with the configuration that matters and the mistakes that still cause flakiness.
- Flaky TestsWhy tests fail intermittently, the six root causes and how to fix each one, how to detect flakiness deliberately, and what to do with a test you cannot fix today.
- Test ReportingGetting results out of the runner and in front of people — JUnit XML, pull request annotations, per-test history, and the numbers worth publishing.
- Testing in Azure DevOps PipelinesA full YAML pipeline with stages, jobs, parallel test slicing and the best built-in test reporting of any CI platform.
- Testing in Bitbucket PipelinesThe simplest CI model of the five — steps, caches, services and parallel groups — with the hard limits you need to design around.
- Testing in JenkinsDeclarative pipelines, parallel stages, agent control and JUnit reporting — how to run a modern test suite on the CI server you probably inherited.