# GitHub Actions ARM runners: what breaks when you switch

> GitHub Actions ARM runners cost less per minute at every Linux size. What breaks when you switch, how to find it in a week, and how to build images.

Source: https://latchkey.dev/learn/speed/run-github-actions-on-arm  
Updated: 2026-09-21

GitHub Actions ARM runners are a one-line change to `runs-on` and a cheaper minute at every Linux size, and the reason most teams have not made the change is a compatibility fear that is now concentrated in three specific places rather than spread across the ecosystem. This page is about finding those three in a week, on one job, before you move anything else.

The labels are `ubuntu-24.04-arm`, `ubuntu-22.04-arm` and `ubuntu-26.04-arm` on Linux, plus `windows-11-arm`. In a private repository they give you 2 vCPU with 8 GB of memory and 14 GB of SSD, which is the same shape as the x64 standard runner, so nothing about the machine forces a change to how the job is written.

The rate is covered separately in [arm64 runner pricing](/learn/cost/github-actions-arm64-runners-pricing), which has the discount at every size and the break-even that goes with it. This page is only about what happens when the job runs.

## Switch one job, not the pipeline

Run the same job on both architectures for a week before you decide anything. A matrix over the two labels costs one extra set of minutes and produces the only evidence that matters, which is whether your own dependencies build and your own tests pass, and it does it without a rollback plan because the x64 leg is still there.

Watch for two different outcomes. A job that fails is good news: the failure names the package, you fix it or you stop, and the whole question is settled in an afternoon. A job that passes but takes noticeably longer is the outcome that needs investigating, because the usual cause is emulation somewhere in the job rather than the architecture being slow.

Compare billed minutes rather than wall clock while you do it, since every job is rounded up to a whole minute and that is what you are actually charged for.

```.github/workflows/ci.yml
# one week, both architectures, no rollback needed
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        runner: [ubuntu-latest, ubuntu-24.04-arm]
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm test
```

## The three things that actually break

First, native modules without an arm64 prebuilt binary. The package falls back to compiling from source, which needs a toolchain to be present and takes minutes instead of seconds, or it fails outright. This is loud and it names itself in the install log, so it is the easiest of the three to deal with.

Second, anything that downloads a release asset with the architecture written into the filename. A setup script fetching a tarball whose name contains `x86_64` or `amd64` will either 404 or, worse, download an x64 binary that cannot execute. Scripts should read `uname -m` and branch, and the value to expect on a Linux arm64 runner is `aarch64` rather than `arm64`, which is the detail that breaks hand-written checks.

Third, container images with no arm64 variant. This is the quiet one. A single-architecture image either fails to pull or runs under emulation, and emulated execution is correct, so nothing in the log says anything is wrong. It is simply several times slower, which turns a cheaper minute into a more expensive job and produces the conclusion that ARM is slow.

| What breaks | How it shows up | The check to run first |
| --- | --- | --- |
| Native module, no arm64 prebuild | Install compiles from source or fails | Read the install log for a build-from-source line |
| Downloaded binary with a fixed arch | 404 on the download, or `exec format error` | Grep your scripts for `x86_64` and `amd64` |
| Container image, amd64 only | Silently slower, sometimes a pull failure | `docker manifest inspect` the image for a linux/arm64 entry |
| A vendored compiled artifact | Runs on x64, refuses on arm64 | Check what is committed as a binary in the repository |

## Emulation is the trap, not the architecture

Modern ARM server cores are competitive with their x64 equivalents, and an interpreted or JIT-compiled test suite generally lands within noise of where it was. What does not land within noise is anything running through QEMU, which is an instruction-level translation and costs a multiple rather than a percentage.

The usual way emulation arrives is a multi-platform Docker build. Setting up QEMU and asking one builder for both `linux/amd64` and `linux/arm64` is correct and it is the slowest possible arrangement, because one of the two architectures is being emulated for the whole build. It is also the arrangement in most copy-pasted workflow snippets.

Build each architecture on a native runner of that architecture and merge the two into one manifest afterwards. That costs a second job and removes the emulation entirely, and it is the change that most often turns a disappointing ARM migration into a successful one.

```.github/workflows/build.yml
# slow: one builder, one architecture emulated through QEMU
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    platforms: linux/amd64,linux/arm64

# fast: each architecture builds natively, then the manifest is merged
jobs:
  build:
    strategy:
      matrix:
        include:
          - runner: ubuntu-latest
            platform: linux/amd64
          - runner: ubuntu-24.04-arm
            platform: linux/arm64
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          platforms: ${{ matrix.platform }}
```

> Merging the two single-platform images into one multi-platform tag is a `docker buildx imagetools create` step in a job that needs both to have finished.

## Why this page has no benchmark of its own

A single arm64 against x64 timing is a statement about one workload. A compile-bound Rust build, a Node test suite waiting on a database, and a Python job that spends its life in a C extension will move in three different directions on the same pair of machines, and publishing one number would invite every reader to apply it to a workload it says nothing about.

There is a second reason, and it is the more important one. The dominant term in a real migration is not the architecture, it is whether anything in the job ended up emulated. A benchmark that did not hit emulation would flatter the switch, and one that did would condemn it, and neither would tell you which of those two your pipeline is.

So the honest instrument is the matrix above, run on your repository for a week. Where a measured comparison does exist in this corpus it is stated as one: [choosing a runner size](/learn/speed/choose-a-github-actions-runner-size) measures the same work at two sizes and says exactly what was run.

## Where the saving actually lands

The discount is real and it is modest at the bottom of the ladder. GitHub prices standard Linux arm64 at $0.005 a minute against $0.006 for x64, read on 21 September 2026, which is about $10 a month on ten thousand minutes. On larger runners the gap widens to nearly 40 percent, which is where an arm64 migration pays for the week you spent on it.

Do not let a small discount buy a slow pipeline. A 16.7 percent saving is erased by a job that takes 20 percent longer, so if the arm64 leg of your matrix is slower than that, the switch costs money at standard size no matter how good it looks on the rate card. The arithmetic and the break-even for every size are on [arm64 runner pricing](/learn/cost/github-actions-arm64-runners-pricing).

One caveat we should state about ourselves: Latchkey sells Linux x64 managed runners and does not sell arm64, so nothing on our price list is an arm64 alternative. If arm64 is the requirement, GitHub's own runners and the vendors that publish an arm64 rate card are the market, and [the runner alternatives roundup](/learn/runners/github-actions-runner-alternatives) lists them.

## FAQ

### Which ARM runner labels does GitHub Actions provide?

On Linux, `ubuntu-24.04-arm`, `ubuntu-22.04-arm` and `ubuntu-26.04-arm`; on Windows, `windows-11-arm` and `windows-11-vs2026-arm`. In private repositories they are 2 vCPU with 8 GB of memory and 14 GB of SSD, matching the standard x64 runner, and in public repositories the standard runners are 4 vCPU with 16 GB.

### Will my build work on ARM without changes?

For most interpreted and JIT-compiled languages, yes. The risk is concentrated in native modules with no arm64 prebuild, scripts that download a binary with the architecture in its filename, and container images published for amd64 only. Run one job on both labels for a week and all three surface immediately.

### Is ARM slower than x64 in CI?

Not inherently. Almost every report of ARM being dramatically slower turns out to be emulation: an amd64 container image or a QEMU multi-platform build running instruction translation for the whole job. Check for a linux/arm64 entry in every image you pull before concluding anything about the architecture.

### How do I build images for both architectures without emulation?

Build each platform on a native runner of that architecture, one job on `ubuntu-latest` and one on `ubuntu-24.04-arm`, then merge the two into a single multi-platform tag with a `docker buildx imagetools create` step once both have finished. Asking one builder for both platforms through QEMU is correct and several times slower.

## References

- [GitHub: hosted runner specifications and every arm64 workflow label (verified 2026-09-21)](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)
- [GitHub: Actions runner pricing, the arm64 and x64 rates compared here (verified 2026-09-21)](https://docs.github.com/en/billing/reference/actions-runner-pricing)
- [Docker: multi-platform builds, native nodes and QEMU emulation (verified 2026-09-21)](https://docs.docker.com/build/building/multi-platform/)
- [actions/runner-images: the Ubuntu 24.04 arm64 image contents (verified 2026-09-21)](https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Arm64-Readme.md)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
