GitHub Actions Matrix Builds
A matrix runs one job definition many times with different variables: every combination of operating system and runtime version, every package in a monorepo, or every shard of a slow test suite. Instead of copy-pasting near-identical jobs, you declare the dimensions under strategy.matrix, and GitHub Actions generates and runs the combinations in parallel.
Matrices are how libraries prove they work on Node 20, 22, and 24 across Linux, macOS, and Windows, and how big test suites finish in minutes instead of an hour. They're also an easy way to burn runner minutes, so it pays to shape them deliberately.
TL;DR
strategy.matrixdefines variables; the job runs once per combination (the Cartesian product).- Access values with
${{ matrix.<name> }}, including inruns-on. includeadds combinations or extra variables;excluderemoves combinations.fail-fast(defaulttrue) cancels remaining jobs on first failure;max-parallelthrottles concurrency.- Build dynamic matrices with
fromJSON()from a previous job's output, for example only the changed packages. - A matrix can generate at most 256 jobs per workflow run.
Quick Example
Test a library across operating systems and Node versions, with one extra experimental combination:
That's 3 × 3 = 9 combinations, minus 1 excluded, plus 1 included: 9 jobs in parallel. A failure on the experimental Node 25 job doesn't fail the workflow.
Core Concepts
Combinations
Each key under matrix is a list. GitHub runs one job per combination of values:
Values can be objects too, which is handy for grouping settings that belong together:
include and exclude
excluderemoves combinations matching all the given keys.includeeither extends existing combinations with extra variables (when all original keys match) or adds new standalone combinations. A matrix consisting only ofincludeentries is an explicit list of jobs.
exclude is applied before include, so include can re-add something exclude removed.
Failure Handling and Throttling
fail-fast: true(the default) cancels in-progress and queued matrix jobs when any one fails. That saves minutes, but it hides whether the failure is specific to one combination. Setfalsefor compatibility matrices where you want the full picture.continue-on-errorat the job level, often driven by a matrix variable, lets specific combinations (nightly or experimental versions) fail without failing the workflow.max-parallellimits concurrent jobs, useful for rate-limited external services or shared test environments.
Dynamic Matrices
A matrix can come from JSON produced by an earlier job, for example to test only the packages changed in a monorepo:
Guard against empty lists: a matrix with no combinations is an error, hence the if:.
Test Sharding
Split a slow suite across parallel jobs using the test runner's sharding support:
Playwright, Vitest, Jest, and pytest (via plugins) all support sharding. Upload each shard's report as an artifact and merge them in a follow-up job.
Required Checks and Matrices
Each matrix job appears as a separate status check (test (ubuntu-latest, node 22)). Requiring every combination individually in branch protection is brittle: renaming a job or changing the matrix breaks required checks. A common pattern adds a single summary job:
Then require only ci-ok. The if: always() ensures it reports failure, rather than being skipped, when a matrix job fails.
Best Practices
Match the Matrix to What You Support
Test the versions and platforms you actually support and ship to. A library supporting Node 20+ needs 20, 22, and 24; an internal service deployed on Node 22 in Linux containers needs one combination.
Put Expensive Dimensions Behind Conditions
Run the full OS × version matrix on main and nightly, and a smaller matrix (Linux plus the current version) on pull requests, using a dynamic matrix or include lists chosen by event. That keeps PR feedback fast and costs predictable.
Cache per Combination
Include matrix variables in cache keys (OS and runtime version), or setup actions' built-in caching will handle it. Sharing one key across OSes produces broken restores.
Name Jobs Clearly
Set name: with matrix values so the checks list and logs say exactly which combination failed.
Common Mistakes
Combinatorial Explosion
Test dimensions independently where interactions are unlikely (browsers on one OS, databases on one Node version) using include lists instead of a full product.
Leaving fail-fast On for Compatibility Testing
With fail-fast: true, one failing combination cancels the rest, so you can't tell whether a bug affects one platform or all of them. Turn it off for compatibility matrices.
Requiring Individual Matrix Checks
Branch protection pinned to test (ubuntu-latest, 20) blocks merges forever after that combination is removed. Use a summary job as the single required check.
FAQ
How many jobs can a matrix create?
Up to 256 jobs per workflow run. Concurrency is also bounded by your plan's concurrent-job limits, so large matrices queue.
Can I use a matrix with reusable workflows?
Yes. Put strategy.matrix on the calling job that uses: a reusable workflow, and pass ${{ matrix.* }} values as inputs. The workflow is invoked once per combination. See reusable workflows.
How do I get outputs from matrix jobs?
Job-level outputs from matrix jobs are overwritten by whichever combination finishes last, so they're unreliable for per-combination data. Have each combination upload an artifact (named with its matrix values), then download and aggregate them in a downstream job.
How do I run only one combination for debugging?
Temporarily narrow the matrix in a branch, or add a workflow_dispatch input and filter the matrix with include built from fromJSON(inputs.combos). Re-running only failed jobs from the Actions UI also works well for flaky single combinations.
Related Topics
- GitHub Actions — The platform overview
- GitHub Actions Workflow Syntax — Jobs, strategy, and outputs
- GitHub Actions Caching — Per-combination cache keys
- Monorepos — Fanning out across packages
- Playwright — Sharding end-to-end tests
- Testing — Test strategy and coverage across platforms