How to Run GitHub Actions on ARM (and Whether You Should)
Every provider prices ARM below x64, frequently by 25 to 40%. It is the largest unclaimed CI saving available, and the reason most teams skip it is a compatibility fear that is mostly out of date.
ARM is cheaper on every runner price list. GitHub lists Linux arm64 2-core at $0.005/min against $0.006 for x64; Blacksmith lists $0.0025 against $0.004; WarpBuild lists arm64 25% below its own x64 rates at every size.
The historical objection was that the ecosystem was not ready. For most interpreted and JIT languages that has not been true for some time. The remaining risk is concentrated in native dependencies and prebuilt binaries, which is testable in an afternoon.
What the saving actually is
| Provider | Linux x64, 2 vCPU | Linux arm64, 2 vCPU | Difference |
|---|---|---|---|
| GitHub-hosted | $0.006/min | $0.005/min | 17% |
| Blacksmith | $0.004/min | $0.0025/min | 38% |
| WarpBuild | $0.004/min | $0.003/min | 25% |
| Semaphore | $0.0075/min | $0.003/min | 60% |
Switch and see what breaks
jobs:
test:
runs-on: ubuntu-24.04-arm # GitHub-hosted ARM
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci && npm testWhat actually breaks
- Native modules without ARM prebuilds. They fall back to compiling from source, which needs a toolchain present and is slower. Usually it works; occasionally it fails outright.
- Downloaded binaries with a hardcoded architecture. Any script fetching an
x86_64release asset breaks. Detect the architecture rather than assuming it. - Docker images without an arm64 variant. A single-arch base image fails to pull, or silently runs under emulation and is dramatically slower.
- Anything vendored as a compiled artifact where you never controlled the build.
Building multi-arch images
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=maxMeasure before you optimise
Pipeline optimisation usually targets the step people assume is slow. Get the real per-step timings first, because the answer is frequently dependency install or a cold cache rather than the build itself.
# per-job timings for the last 20 runs
gh run list --limit 20 --json databaseId,conclusion,createdAt,updatedAt \
--jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'
# per-step timing inside one run
gh run view <run-id> --log | grep -E "^\S+\s+.*Run |##\[group\]" | head -40Frequently asked questions
Are ARM runners cheaper?
Will my build work on ARM?
Is ARM slower than x64?
How do I build images for both architectures?
platforms: linux/amd64,linux/arm64. For speed, build each architecture on a native runner and merge the manifest rather than emulating, since QEMU is several times slower than native.