GitHub Actions matrix strategy

A GitHub Actions matrix runs the same job many times, once for each mix of values you list, like Node versions or operating systems. GitHub starts those jobs in parallel, as far as your runners allow.

What a matrix does

Say you want to test on Node 20, 22 and 24. You could copy the job three times. A matrix does it for you.

You list values under strategy.matrix. GitHub makes one job for each combo. Two lists of 3 values each make 3 × 3 = 9 jobs.

A GitHub matrix can make up to 256 jobs per workflow run. That limit is in GitHub's Actions limits.

Matrices are good for:

  • testing on many language versions, like Node, Python or Go;
  • testing on many systems, like Linux and macOS;
  • building for many CPUs, like x86-64 and arm64;
  • splitting a long test suite into parts that run side by side.

A first matrix example

Here is a small GitHub action matrix. It tests three Node versions:

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20, 22, 24]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

${{ matrix.node }} is the value for this one job. GitHub also adds it to the job's name, so you see test (20), test (22) and test (24).

A matrix can have more than one key. Each key adds a new axis. Add os: [ubuntu-latest, macos-latest] to the example above, and you get 6 jobs: three Node versions on each system. Keep an eye on the count. Each new axis multiplies the jobs, and each job uses minutes.

include and exclude

You don't always want every combo. Two keys help.

  • exclude drops combos you don't need.
  • include adds an extra combo, or adds a value to combos that match.
strategy:
  matrix:
    os: [ubuntu-latest, macos-latest]
    node: [20, 22, 24]
    exclude:
      - os: macos-latest
        node: 20          # skip old Node on macOS
    include:
      - os: ubuntu-latest
        node: 24
        coverage: true    # only this job uploads coverage

This makes 5 jobs, not 6. Then a step can check if: matrix.coverage to run only once.

Use exclude often. A smaller matrix finishes sooner, waits less in the queue and uses fewer minutes.

fail-fast and max-parallel

Two more keys control how the matrix runs.

GitHub Actions fail-fast is on by default. If one matrix job fails, GitHub cancels the jobs still running or queued. That saves time when one failure means all will fail. Turn it off when you want to see every result:

strategy:
  fail-fast: false
  max-parallel: 2
  matrix:
    node: [20, 22, 24]

max-parallel caps how many matrix jobs run at the same time. Use it when jobs share something with a limit, like a test server or an API.

You can also set continue-on-error on a job. Then a failure there doesn't cancel the rest. It is handy for a job you mark as experimental.

Want to split one slow test suite? Use a matrix of shard numbers. Each job runs one part:

strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - uses: actions/checkout@v4
  - run: npx jest --shard=${{ matrix.shard }}/4

Four jobs, each with a quarter of the tests. If you have four free runners, the suite finishes in about a quarter of the time.

How many jobs run at once

A matrix of 20 jobs doesn't mean 20 jobs run at once. Two things cap it:

  • Your plan, on GitHub-hosted runners. Each plan allows a set number of jobs at once. macOS jobs have a lower cap.
  • Your runners, on self-hosted runners. One runner takes one job. With 3 free runners, 3 jobs run, and the rest wait.

Jobs that can't start yet wait in the queue. If they wait a long time, read waiting for a runner. Plan numbers are in GitHub Actions limits.

Matrix jobs on your own machines

A matrix works the same on your own runners. Just change runs-on. With Codemany, each matrix job takes a fresh, single-use runner on any free machine.

How many jobs fit? Each job needs about 4 GB of memory, 2 CPU cores and 15 GB of disk. An 8 GB Mac runs 1 job at a time, and a 16 GB Mac runs 3. To run more matrix jobs at once, add machines. See how many jobs per machine.

You can even spread one matrix across your Macs and your Linux boxes:

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [macOS, Linux]
    runs-on: [self-hosted, "${{ matrix.os }}", codemany]
    steps:
      - uses: actions/checkout@v4
      - run: make test

The macOS job goes to a Mac. The Linux job goes to a Linux machine. Both run at the same time if both are free. To set one up, follow Get started.

On your own machines, a bigger matrix doesn't cost more per minute. Codemany is $20 a month for 2 machines, and each extra machine is $10 a month. More machines let more matrix jobs run side by side.

Questions

How many jobs can a matrix make?

Up to 256 per workflow run.

What does fail-fast do?

It cancels the other matrix jobs when one fails. It is on by default.

Can matrix jobs run on self-hosted runners?

Yes. Each job takes a fresh Codemany runner on any free machine.

Read next: single-use runners, GitHub Actions limits, waiting for a runner, jobs per machine and Get started.

Start a 14-day trial No card needed. $20 a month for 2 machines after that.