Matrix Strategy in GitHub Actions: Multiple OSs and Versions
Run several operating systems and runtime-version combinations with the GitHub Actions matrix strategy, plus include, exclude, fail-fast and parallel limits.
Netbay Infrastructure Team
Netbay Engineering
On this page
Testing against one Node version on one OS is how subtle portability bugs slip through. GitHub Actions solves this with the matrix strategy: a single job definition expanded into many parallel variants, each a different combination of the named factors. One matrix entry can test three OSs times four Node versions, and GitHub fans each combination out as its own runner. This post covers matrix basics, when to include and exclude, and the performance pitfalls.
The core idea
A matrix is defined under jobs.<name>.strategy.matrix. You list factors with arrays of values, and GitHub builds the Cartesian product — every combination of one value from each factor. Each combination becomes a separate job instance with a unique index. Within the job, the current combination is available through the matrix context.
The most common factors are operating systems and runtime versions. Here is a Node testing matrix:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04, windows-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm testThat definition produces nine parallel jobs (three OSs times three Node versions), each running the same steps with a different combination. The matrix context gives you matrix.os and matrix.node to parameterize actions, cache keys, and artifact names so they do not collide.
Carrying matrix values into later phases
A common need is to build on the matrix and then upload results keyed by the combination. Use matrix values inside artifact names or in a later job that references the matrix output:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
node: [18, 20]
steps:
- run: npm test
- uses: actions/upload-artifact@v4
with:
name: test-report-${{ matrix.os }}-${{ matrix.node }}
path: test-report.xmlMatrix-driven artifact names let a later job download the right one per combination, or you can aggregate them. Keep the names unique — the matrix values are the natural key.
include, exclude and the eager matrix
GitHub lets you refine the product. exclude removes specific combinations that do not make sense (for example, skip Node 22 on an OS that does not support it yet). include adds extra jobs that are not part of the pure product, or merges extra fields onto matching combinations. include is subtle: when you include an object with a unique matrix key, it becomes an extra root job; when it matches an existing combination, it adds its extra fields to that job.
A common use of include is to attach a framework flag to a specific combination. Here is a matrix with an exclude and an include:
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-24.04, macos-latest]
node: [18, 20]
exclude:
- os: macos-latest
node: 18
include:
- os: ubuntu-22.04
node: 20
experimental: trueFail-fast and max-parallel
By default, when one matrix job fails, GitHub cancels all the in-flight matrix jobs — that is fail-fast: true. If you want a job to keep running even when a sibling fails, set fail-fast: false at the strategy level. This is valuable when a matrix is doing independent reporting and one failure should not abort the rest. You can also cap concurrency with max-parallel to avoid hammering resources.
strategy:
fail-fast: false
max-parallel: 4
matrix:
os: [ubuntu-22.04, ubuntu-24.04]
node: [18, 20, 22]Pitfalls to watch
Three pitfalls deserve attention. First, matrix jobs are independent runners — they re-checkout and re-install, so total time is not reduced by a single save; it is distributed across parallel machines. Second, the matrix product grows fast: three factors of four values each is 64 jobs. Keep factors aligned to real compatibility needs, and use exclude generously. Third, if a later job must consume the matrix results (like an aggregate report), give the matrix job outputs and read them with needs, or upload artifacts with unique names. And remember that self-hosted runners can be part of a matrix too — but only mark the runner if it is actually available for those labels, or the job will sit in the queue.
Takeaway
Matrices give you confidence that code really works across platforms and versions, at the cost of parallel resources. Keep factors to real compatibility needs, use exclude and include to prune nonsense, and let fail-fast match how much you want individual failures to block the fleet. When you want eyeballs on a matrix, the parallel runners on a Netbay Ubuntu VPS are a cheap way to add capacity — spin one up in under a minute at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance