How to Balance Test Shards by Timing in GitHub Actions
Splitting tests by recorded duration, not file count, keeps every shard finishing together so the slowest one is no longer the bottleneck.
Record per-test timings, then partition the suite so each shard holds roughly equal total time. Runners that support a timings file (Playwright, Knapsack-style splitters) do this automatically.
Steps
- Persist per-test durations from a prior run as an artifact.
- Feed the timings to a splitter so each shard holds equal time.
- Fall back to even file splits when no timing data exists yet.
Workflow
.github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --shard=${{ matrix.shard }}/4
- uses: actions/upload-artifact@v4
with:
name: timings-${{ matrix.shard }}
path: test-results/timings.jsonGotchas
- Timing data drifts as tests change; refresh it periodically or shards re-skew.
- Suffix the artifact name with the shard, or v4 rejects the duplicate upload.
Related guides
How to Shard Tests Across Parallel Jobs in GitHub ActionsSplit a slow test suite across parallel GitHub Actions jobs with a matrix shard index, running each fraction…
How to Profile a Slow Workflow With Step Timings in GitHub ActionsFind the slowest part of a GitHub Actions run by reading per-step durations in the timing graph and adding a…