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.

  1. 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.
  2. 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.
  3. 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

StrategyHow the split is computedBalanceStability as tests change
File orderSort the file list, then deal files round robin or in contiguous blocksPoor when a few files hold most of the runtimeAdding one file shifts every later assignment
Hash of test idHash each test name or path, take the result modulo the shard countGood on large suites, uneven on small onesStable, only the changed tests move
Duration weightedRead recorded per test durations and pack shards to equal predicted timeBest available, and only as good as the timing dataShifts whenever the timings are refreshed
Directory or tagAssign by folder, suite name, or a marker on the testDepends entirely on how the tests are organizedStable 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.

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 results

The 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:

ShardsTest minutes per shardJob wall clockTotal machine minutesSetup share
14042422 of 42
22022444 of 44
41012488 of 48
8575616 of 56
162.54.57232 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.

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.