The Slack thread that starts it
Your Vitest suite crossed 20 minutes sometime last quarter, and this week someone posted the inevitable message in the platform channel: "can't we just shard it?" Yes, you can. Vitest has first-class sharding built in, and when it's wired up correctly a 20-minute suite genuinely drops to 6 or 7 minutes of wall clock. When it's wired up carelessly, you get four jobs that each pay full setup cost, coverage numbers that quietly stopped meaning anything, and a class of test failures that only reproduce on shard 3 of 4.
I've set this up on enough repos to know where the bodies are buried. This post covers what --shard actually does, the full GitHub Actions wiring including report merging, and the failure modes that don't show up in the docs.
One thing before you shard anything: sharding is the fix for a suite that's slow because there's too much work for one machine. If your suite is slow because three test files eat 80% of the runtime, or because CI runs single-threaded while your laptop uses 10 cores, fix that first. We wrote up how to read Vitest's duration breakdown before touching config, and it applies double here. Sharding a badly configured suite gets you four badly configured suites.
What --shard actually does
The flag takes a fraction:
vitest run --shard=1/4
That tells Vitest "there are 4 shards total, run the files belonging to shard 1." Vitest collects the test file list, sorts it, and deals files out across shards. Each shard is a completely independent Vitest process, typically on a completely independent machine, that has no idea the other shards exist.
Three properties fall out of that design, and all three matter:
- Distribution is per file, not per test. A file with 400 fast unit tests and a file with 12 slow integration tests each count as one unit. Vitest is not balancing by duration.
- Shards are deterministic for a given file list. The same commit produces the same file-to-shard assignment. Add or delete a test file and the assignment reshuffles, which means a test can migrate to a different shard because someone touched an unrelated spec.
- Each shard pays full startup. Dependency install, config load, environment setup, global setup hooks. All of it, once per shard.
Also worth knowing: --shard refuses to run in watch mode. It's a CI feature. Nobody should be sharding on their laptop, and Vitest agrees.
The CI wiring, including the part everyone skips
Sharding splits execution, which means it also splits reporting. If each shard prints its own summary and writes its own JUnit file, you now have four partial views of one logical test run. Vitest's answer is the blob reporter: each shard writes a machine-readable blob, and a final job merges the blobs and emits whatever reports you actually want.
Here is one shard job, expanded. In a real workflow you'd collapse the four shard jobs into a strategy.matrix over shard indexes and reference the values with GitHub's expression syntax; I'm showing a single expanded job so the mechanics are visible, and because the run step only reads env vars, it's identical in both forms.
jobs:
test-shard-1:
runs-on: ubuntu-latest
env:
SHARD_INDEX: 1
SHARD_TOTAL: 4
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx vitest run --reporter=blob --shard="$SHARD_INDEX/$SHARD_TOTAL"
- uses: actions/upload-artifact@v4
if: always()
with:
name: blob-report-1
path: .vitest-reports/
Two details in there that bite people. First, upload-artifact@v4 requires unique artifact names across jobs, so the shard index has to be part of the name. Second, the upload step needs if: always(), because the blob from a failing shard is the blob you most want to see.
Then the merge job:
merge-reports:
if: always()
needs: [test-shard-1, test-shard-2, test-shard-3, test-shard-4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
pattern: blob-report-*
path: .vitest-reports
merge-multiple: true
- run: npx vitest run --merge-reports --reporter=junit --outputFile=junit.xml
The merge step replays the blobs through whatever reporters you configure, so you get one JUnit file, one console summary, one anything, describing the whole logical run. On Vitest 2 and later, blob reports also carry coverage data, so --merge-reports regenerates a combined coverage report instead of leaving you to stitch istanbul JSON files together by hand. If you're on Vitest 1.x and need merged coverage, that stitching is your problem, and it's a good reason to upgrade.
One hard rule: the merge job must run the same Vitest version as the shards. Blob format is internal and unversioned as far as you're concerned. A lockfile shared across jobs handles this for free; a merge job that installs "latest" handles it until the first minor release after you stop paying attention.
The pitfalls the docs won't warn you about
Shards are balanced by file count, not by time
This is the big one. Because Vitest deals files, not durations, a suite with a few heavyweight integration files will produce shards where one finishes in 3 minutes and another takes 11. Your wall clock is the slowest shard, so you've paid for four machines and gotten the speedup of two.
Before picking a shard count, look at your per-file durations. If the top 5 files account for half the runtime, splitting those files does more than adding shards ever will. And once you're sharded, watch shard durations over time: they drift as the suite grows, and nobody notices until the imbalance gets absurd.
Setup cost multiplies
Four shards means four checkouts, four npm ci runs, four global-setup executions. If install plus setup takes 2 minutes, that's an 8-minute tax on every push, and a 2-minute floor under your wall clock no matter how many shards you add. This is Amdahl's law wearing a CI costume. Aggressive dependency caching stops being nice-to-have here; our runner cache benchmarks show how much of that tax you can claw back.
Global setup deserves special suspicion. If globalSetup runs migrations or seeds a database, it now does so once per shard. Against a shared staging database, four shards doing concurrent setup is a race condition you built on purpose.
Sharding exposes cross-file coupling
A test that passes only because another file ran before it and left the right state behind will fail the moment sharding puts those files on different machines. The failure looks flaky and shard-dependent: green on 2 shards, red on 4, green again on 4 after an unrelated file gets added and the deal reshuffles. If sharding "introduced" flakiness in your suite, it almost certainly revealed coupling that was already there. The debugging approach is the same one we describe for ordering-dependent flaky tests: make the ordering assumption explicit, then delete it.
The related trap is combining sharding with isolate: false for extra speed. That setting makes test files share module state within a worker, which amplifies exactly the coupling that sharding punishes. It can be safe, but only if your suite was honest to begin with.
Your reporting pipeline needs to know
Anything downstream that consumes test results, whether that's a flaky-test detector, a compliance evidence trail, or a dashboard, needs either the merged report or all shard reports tagged as parts of one run. Feed it one shard's JUnit file and it will happily conclude that three quarters of your suite vanished. If you're sending results to BuildPulse, upload the merged JUnit output from the merge job and everything correlates cleanly; flaky-test detection in particular gets more useful under sharding, because shard-dependent failures are exactly the kind of intermittent signal humans dismiss as "CI being weird."
Sharding versus a bigger machine
Here's the question platform engineers should ask before writing the matrix: is the suite slow because one machine can't parallelize it, or because one machine is too small?
Vitest already parallelizes across cores within a single run. On a 2-core hosted runner, that means almost nothing. On a 16-core runner, a suite that took 20 minutes often lands at 5 or 6 with zero workflow changes, no report merging, no artifact plumbing, and you pay setup cost exactly once. In our runner benchmarks, moving to larger, faster machines routinely beat naive sharding on both wall clock and total cost, because four shards on small runners quadruple the setup tax while a big runner amortizes it.
The honest decision tree looks like this:
- Suite under ~10 minutes on a right-sized machine: don't shard. The complexity isn't worth it.
- Suite CPU-bound and runners are small: get bigger runners first. Cheapest speedup available.
- Suite over ~15 minutes on a machine it can actually saturate: shard, start at 2 or 3, and measure shard balance before adding more.
- Suite dominated by a few slow files: split those files. Sharding around them just moves the bottleneck to whichever shard draws the short straw.
Sharding and bigger machines also compose. Two shards on fast 8-core runners frequently beats four shards on stock 2-core ones, at lower total spend.
The checklist
If you take one thing from this: vitest sharding is an execution strategy and a reporting problem, and most teams only implement the first half. Before you call it done, verify that a failing test on any shard fails the merged report, that coverage reflects the whole suite and not one shard's slice, that the merge job runs even when shards fail, and that shard durations are within shouting distance of each other. Get those four right and sharding is one of the best wall-clock levers you have. Skip them and you've built a faster way to not know whether your tests passed.



