Git, CI/CD & GitOpsCI · Pipeline PerformanceParallelism
Parallelism and matrix — fan-out, fan-in, and the trade-offs
What you'll learn
- Apply strategy.matrix with include and exclude to fan a job out across N combinations
- Compute the wall-clock duration of a matrix as max(shard durations) plus fan-in overhead
- Distinguish fail-fast from no-fail-fast and recognise when to disable fail-fast
- Recognise the cost trade-off: more shards means more runners and more cache writes
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The matrix strategy is the GitHub Actions mechanism for fanning a job out across N combinations of variables. Each combination runs on its own runner. The fan-out gives parallel execution; the fan-in gives a single pass/fail signal that the rest of the workflow can depend on. The trade-offs are runner cost, cache writes per shard, and the rule that wall-clock duration is the longest shard, not the average.
The matrix fan-out
The matrix is declared in the strategy.matrix block of a job.
Each entry in matrix is a list of values; the runner produces the
Cartesian product and runs one job per combination.
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20, 22]
exclude:
- os: macos-latest
node: 18
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${ matrix.node }
- run: npm ci
- run: npm test
The matrix expands to six jobs: (ubuntu,18), (ubuntu,20),
(ubuntu,22), (macos,20), (macos,22), and the exclude removes
(macos,18) for a reason documented elsewhere (Node 18 has reached
end-of-life on macOS runners). Each combination runs on its own
runner; the runners do not share state during the run.
flowchart TB
A["strategy.matrix: os x node"] --> B1["ubuntu-18"]
A --> B2["ubuntu-20"]
A --> B3["ubuntu-22"]
A --> B4["macos-20"]
A --> B5["macos-22"]
B1 --> C["Aggregate via needs:"]
B2 --> C
B3 --> C
B4 --> C
B5 --> C
C --> D["Single pass/fail signal"]
The diagram shows the fan-out and fan-in. Five shards run in parallel; the aggregate job waits for all five and emits a single result.
Wall-clock is the longest shard
The wall-clock duration of a matrix is max(shard durations) plus
fan-in overhead. A matrix with five shards where four take three
minutes and one takes ten minutes has a wall-clock duration of
ten minutes plus the fan-in overhead, not three minutes.
This is the key constraint on matrix sizing: the wall-clock saving stops as soon as one shard dominates. A test suite split into ten shards where one shard contains the slowest tests will see wall-clock duration equal that shard’s duration. The right split is the one that balances shard durations, not the one that maximises the shard count.
The cache key per shard matters here too. Each shard restores its
own cache; a cache key that includes ${ matrix.os } and
${ matrix.node } keeps the shards from colliding.
Fail-fast and the trade-off
fail-fast: true (the default) cancels in-flight shards as soon
as one fails. The wall-clock duration drops to the failing shard’s
duration plus cancellation overhead, which is useful when the
team wants fast feedback. fail-fast: false lets all shards
finish regardless of individual failures, which is useful when the
team wants the complete failure picture (for flaky-test diagnosis,
for example).
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3, 4, 5, 6, 7]
The default is right for most workflows. fail-fast: false is
right when the team needs every shard’s result to diagnose a
recurring failure or to compute coverage across all shards.
The cost trade-offs
Three costs grow with matrix size:
- Runner cost per workflow run. Each shard is a separate runner. A 20-shard matrix is 20 runners’ worth of compute per workflow run. The cost-per-job (one of the baseline metrics) rises linearly with shard count.
- Cache writes per run. Each shard writes its cache on miss. A 20-shard matrix that misses on every shard writes 20 cache entries. The cache store retention limit (10 GB per repository on GitHub Actions) is consumed faster.
- Queue time when the pool is saturated. A 20-shard matrix against a pool with five free runners queues fifteen shards. The queue time appears in the baseline as an increase in the queue-time metric, even though the matrix itself is fast.
The right matrix size balances wall-clock saving against the three costs. A team that quadruples the shard count and halves the wall-clock has doubled the cost-per-job; whether that is a win depends on the baseline.
Production discipline
- Balance shard durations. The wall-clock saving stops when one shard dominates. Profile the shards and adjust the split.
- Include the matrix axes in the cache key.
${ matrix.os }and${ matrix.node }keep the shards from colliding. - Match shard count to runner-pool capacity. A matrix larger than the free pool queues. The wall-clock saving is consumed by queue time.
- Use
fail-fast: falseonly when needed. The default is right for most workflows.
Cross-course references
- Linux for Production Sysadmins - Part XXXIV (ConfigMgmt) applies the matrix pattern to per-distribution package builds.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) applies it to per-inventory-group molecule runs.
- Terraform for Production Sysadmins - Parts IX-XII (State) apply it to per-workspace plan/apply.
Quiz
Knowledge check · 4 questions
Q1. A test matrix splits into ten shards. Nine shards take three minutes; one shard takes fifteen minutes. What is the wall-clock duration?
Q2. A 20-shard matrix against a runner pool with five free runners runs all twenty shards in parallel.
Q3. Explain why the cache key for a sharded matrix must include the matrix axes (os, node-version, shard index), and what failure mode follows from omitting them.
Q4. Diagnose why a newly-introduced matrix has worse wall-clock duration than the serial workflow it replaced, and propose the fix.
Team G's test suite used to run serially in 18 minutes on one runner. After splitting into a matrix of six shards, the workflow now takes 16 minutes wall-clock and the monthly CI bill has tripled. The runner pool has three free runners at peak.
Passing score: 75%. Answers are checked in this browser.