Test Sharding
Test sharding splits a test suite into disjoint slices that run on separate machines at the same time, then merges the shard results into one verdict.
Test sharding splits a test suite into disjoint slices, called shards, and runs each shard on a separate machine at the same time. Every test lands in exactly one shard, and the per shard results are merged afterwards into a single pass or fail verdict for the commit.
Sharding trades total machine time for wall clock time. The same tests still execute, and every shard repeats the fixed setup work of a job, so the sum of machine minutes goes up while the time a developer waits for a verdict goes down.
Definition
A sharding scheme is defined by three properties. Break any one of them and the run stops being a faithful substitute for executing the whole suite on one machine.
- Disjoint and complete. The shards form a partition of the suite. Every test appears in exactly one shard, and the union of the shards is the entire suite. Overlap burns minutes twice and double counts flaky results. A gap lets an untested change ship behind a green check.
- Deterministic. The same suite at the same shard count produces the same assignment on every run. A failure reported by shard 3 can then be reproduced by running shard 3 alone on a laptop.
- Merged reporting. Each shard reports its own outcome, so the workflow needs a step that collects the per shard results into one status. Without it, a passing summary can hide a red shard.
Most test runners express the split as an index and a total, written --shard=2/4 for the second of four slices. Runners without such a flag are sharded from the outside: the workflow generates the file list, splits it, and passes one slice to the test command as arguments.
How tests are assigned to shards
| Strategy | How the split is computed | Balance | Stability as tests change |
|---|---|---|---|
| File order | Sort the file list, then deal files round robin or in contiguous blocks | Poor when a few files hold most of the runtime | Adding one file shifts every later assignment |
| Hash of test id | Hash each test name or path, take the result modulo the shard count | Good on large suites, uneven on small ones | Stable, only the changed tests move |
| Duration weighted | Read recorded per test durations and pack shards to equal predicted time | Best available, and only as good as the timing data | Shifts whenever the timings are refreshed |
| Directory or tag | Assign by folder, suite name, or a marker on the test | Depends entirely on how the tests are organized | Stable until the directories are reorganized |
The wall clock of the job set is the duration of the slowest shard, so balance decides the result more than the shard count does. A four shard split where one shard inherits 25 minutes of a 40 minute suite finishes in about 27 minutes, while an even split of the same suite finishes in about 12 minutes. Duration weighted assignment exists to close that gap.
Related forms of parallelism
Two nearby mechanisms are often confused with sharding.
Worker level parallelism runs several test processes on a single machine, sized by the vCPU count of that machine. The workers share one checkout and one dependency install, so they add no extra setup cost, and their ceiling is the size of that machine.
A build matrix fans one job definition out over a list of configurations, such as three language versions or two architectures. Each leg runs the whole suite under a different configuration. Sharding borrows the same matrix syntax to fan out over slices of a single configuration, which is why the two get mixed up.
Why the wall clock flattens
Every shard repeats the fixed work of a job: provisioning a machine, checking out the repository, restoring caches, and installing dependencies. Call that fixed cost F and the pure test time T. One job takes F plus T. N shards take F plus T/N at best, and the machine time billed across the fleet is N times F, plus T. Returns flatten once T/N falls near F.
Example
A four shard split in GitHub Actions is a matrix over the shard index. Setting fail-fast: false keeps the other three shards running when one of them fails, which is what makes the merged report worth reading.
name: test
on:
push:
jobs:
test:
runs-on: warp-ubuntu-latest-x64-4x
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx jest --shard=${{ matrix.shard }}/4 --reporters=default --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_NAME: results-${{ matrix.shard }}.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: results-${{ matrix.shard }}
path: results-${{ matrix.shard }}.xml
merge:
needs: test
if: always()
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/download-artifact@v4
with:
pattern: results-*
merge-multiple: true
path: results
- run: ./scripts/merge-junit.sh resultsThe matrix creates four jobs from one definition, each carrying a different value of matrix.shard. The test command receives that index and the total, so the third job runs the third quarter of the suite and skips the rest. Artifact names carry the shard index because version 4 of the upload action rejects two uploads under one name in the same run (actions/upload-artifact, checked on 2026-08-13).
if: always() on the upload step preserves the report from a failing shard, and the same condition on the merge job lets it run after a red shard instead of being skipped. The merge job pulls every artifact matching the pattern into one directory and produces a single result file, which is the merged reporting property from the definition. The matrix sharding guide walks through the shard aware command and the merge step in full.
What the split does to the clock
Take a suite that needs 40 minutes of pure test time, with 2 minutes of setup per job, split evenly:
| Shards | Test minutes per shard | Job wall clock | Total machine minutes | Setup share |
|---|---|---|---|---|
| 1 | 40 | 42 | 42 | 2 of 42 |
| 2 | 20 | 22 | 44 | 4 of 44 |
| 4 | 10 | 12 | 48 | 8 of 48 |
| 8 | 5 | 7 | 56 | 16 of 56 |
| 16 | 2.5 | 4.5 | 72 | 32 of 72 |
Wall clock drops from 42 minutes to 12 at four shards and to 4.5 at sixteen, while machine minutes rise from 42 to 72. Setup is what bends the curve: at sixteen shards, 32 of the 72 machine minutes go to work that has nothing to do with running tests.
Two documented limits cap the fan out. A single matrix produces at most 256 jobs per workflow run, and jobs beyond the concurrency ceiling of the account wait in the queue rather than starting (GitHub Actions limits, checked on 2026-08-13). A split wider than the available concurrency serializes itself, and the wall clock stops improving while the machine minutes keep climbing.
Related Terms
- Shard a test suite across a GitHub Actions matrix: the shard index matrix, a shard aware test command, and the merge job written out end to end.
- Build matrix: the matrix syntax that sharding borrows, and how fanning out over configurations differs from fanning out over slices.
- Sharding pytest suites across runners: worker processes on one machine against shards across jobs, and how the two levers combine.
- GitHub workflow syntax for strategy.matrix: the documented behavior of
matrix,include,exclude, andfail-fast. - WarpBuild reports documentation: per job duration and queue time metrics, which are the inputs a duration weighted split needs.
- WarpBuild cloud runners documentation: the runner labels and machine sizes a shard matrix can target.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is the difference between test sharding and running tests in parallel?
Parallel workers run several test processes on one machine and share its checkout, its installed dependencies, and its vCPU budget. Sharding splits the suite across separate machines, so each shard pays its own setup cost and the ceiling is the number of jobs that can run at once rather than the size of one machine. Large suites usually use both.
How many shards should a test suite use?
Add shards while the pure test time per shard stays well above the fixed setup time of a job. A suite with 40 minutes of tests and 2 minutes of setup per job lands around four to eight shards. Past that point the total machine minutes climb quickly while the wall clock barely moves.
What happens when one shard fails?
With fail-fast set to false the remaining shards keep running, so one run reports every failure in the suite instead of stopping at the first. The merge job needs if always() so it still collects artifacts from failed shards, otherwise the merged report is skipped whenever a shard is red.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.