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

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

exclude is applied before include, so include can re-add something exclude removed.

Failure Handling and Throttling

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

References