GitHub Actions Matrix Builds
A matrix build runs the same job multiple times with different combinations of values — like different programming language versions, operating systems, or database types. Instead of writing three separate jobs to test on Node.js 18, 20, and 22, you write one job with a matrix and GitHub creates all three automatically.
The Problem Matrix Solves
Without matrix builds, testing across multiple environments means copying and pasting jobs:
Without matrix (repetitive and hard to maintain):
jobs:
test-node18:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with: { node-version: '18' }
- run: npm test
test-node20:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm test
test-node22:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm test
With matrix builds, one job definition produces all three:
With matrix (clean and scalable):
jobs:
test:
strategy:
matrix:
node-version: [18, 20, 22]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
How Matrix Works
The strategy.matrix block defines lists of values. GitHub creates one job instance for each value in the list. The matrix context inside the job provides the current iteration's value.
matrix:
node-version: [18, 20, 22]
GitHub generates:
Job 1 → node-version = 18
Job 2 → node-version = 20
Job 3 → node-version = 22
All three run in parallel.
Multi-Dimensional Matrix
Add multiple variables to create every possible combination:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
GitHub generates 6 jobs — one for each combination:
ubuntu-latest + node 18
ubuntu-latest + node 20
windows-latest + node 18
windows-latest + node 20
macos-latest + node 18
macos-latest + node 20
Use ${{ matrix.os }} in runs-on and ${{ matrix.node-version }} in the setup step:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Excluding Specific Combinations
Some combinations may not be valid or useful. The exclude key removes specific combinations from the matrix:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
exclude:
- os: macos-latest
node-version: 18
This removes the macOS + Node 18 combination, leaving 5 jobs instead of 6. Use this when a combination is known to be incompatible or redundant.
Including Extra Combinations
The include key adds specific combinations that are not part of the main matrix, or adds extra variables to existing combinations:
strategy:
matrix:
node-version: [18, 20]
include:
- node-version: 22
experimental: true ← Extra variable only for this entry
Now Node 22 is also tested, and only that job has the experimental variable set to true. You can use this in a condition later:
steps:
- name: Run tests
run: npm test
continue-on-error: ${{ matrix.experimental == true }}
Node 22 tests are allowed to fail without stopping the overall job.
Controlling Failure Behavior with fail-fast
By default, GitHub cancels all remaining matrix jobs the moment any one of them fails. This is called fail-fast and it saves runner minutes when an early failure reveals a fundamental problem.
Disable fail-fast when you want every combination to run to completion regardless of failures:
strategy:
fail-fast: false
matrix:
node-version: [18, 20, 22]
With fail-fast: false, a failure on Node 18 does not cancel the Node 20 and Node 22 jobs. You get complete results for all versions.
Controlling Parallelism with max-parallel
By default, all matrix jobs run simultaneously. Limit how many run at once using max-parallel:
strategy:
max-parallel: 2
matrix:
node-version: [18, 20, 22]
With max-parallel: 2, only two jobs run at once. The third waits until one of the first two finishes. Use this when the target system cannot handle too many simultaneous connections.
Practical Matrix Example — Python Test Matrix
name: Python Test Matrix
on: [push, pull_request]
jobs:
test:
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12']
os: [ubuntu-latest, windows-latest]
exclude:
- os: windows-latest
python-version: '3.10'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- run: pytest --tb=short
This produces 5 test jobs: 3 Python versions on Ubuntu, plus Python 3.11 and 3.12 on Windows.
Matrix Build Benefits at a Glance
Without Matrix | With Matrix
------------------------|---------------------------
One job per version | One job definition, N runs
Hard to add versions | Add a value to the list
Parallel by hand only | Parallel automatically
Inconsistent structure | Identical structure per job
