How Do I Run ARM64 Integration Tests?
Run the test job on a native ARM64 runner so the service under test and harness share the target architecture. warp-ubuntu-latest-arm64-8x is $0.012 a minute.
Last verified:
Run the integration test job on a native ARM64 runner so the service under test, its service containers, and the test harness all execute on aarch64. Point runs-on at a Linux ARM64 label such as warp-ubuntu-latest-arm64-8x, which carries 8 vCPU and 32 GB at $0.012 per minute (WarpBuild cloud runners, checked on 2026-08-13).
Answer
An integration test is only meaningful when the binaries under test are the binaries you ship. On an emulated host the application code runs through instruction translation while the service containers, the language runtime, and the kernel behave like the x86-64 host underneath, so the run tells you about the emulator as much as about your build. A native ARM64 runner removes that gap: the kernel is aarch64, the container runtime pulls linux/arm64 images, and the assertions execute against the same instruction set as your production hosts.
WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, and the Linux ARM64 line runs from 2 vCPU to 32 vCPU across Ubuntu 24.04 and Ubuntu 26.04 (Linux ARM64 runner catalog). Service containers work the same way they do on x86-64: GitHub Actions starts each entry under services as a Docker container on the runner and maps its ports into the job (GitHub service containers documentation).
name: integration-tests-arm64
on:
pull_request:
push:
branches: [main]
jobs:
integration:
runs-on: warp-ubuntu-latest-arm64-8x
timeout-minutes: 20
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: testpw
POSTGRES_DB: app_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-retries 10
env:
DATABASE_URL: postgres://postgres:testpw@localhost:5432/app_test
REDIS_URL: redis://localhost:6379
steps:
- uses: actions/checkout@v4
- name: Confirm the architecture of the job and the services
run: |
uname -m
docker exec "$(docker ps -qf name=postgres)" uname -m
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: WarpBuilds/cache@v1
with:
path: ~/.npm
key: npm-arm64-${{ hashFiles('package-lock.json') }}
- run: npm ci
- name: Run migrations
run: npm run db:migrate
- name: Integration suite
run: npm run test:integration -- --workers=4
- uses: actions/upload-artifact@v4
if: always()
with:
name: integration-logs-arm64
path: test-results/Both uname -m calls print aarch64. Cache is enabled by default on Linux runners including the ARM64 line, and WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 (caching documentation).
Detail
The two failure classes emulation hides
Architecture-specific race conditions. aarch64 has a weaker memory model than x86-64, so a load or store that a compiler is free to reorder on ARM stays ordered on an x86-64 host. Code that omits an acquire or release barrier can pass every run on the x86-64 machine and fail on real ARM hardware (Linux kernel memory-barriers documentation). QEMU user-mode emulation, which docker/setup-qemu-action registers through binfmt handlers (docker/setup-qemu-action), translates guest instructions on that same x86-64 host, so the ordering guarantee your code accidentally depends on is still there. Concurrency bugs in connection pools, cache invalidation, and lock-free queues are the ones that surface first on native hardware.
Native module load failures. Anything with a compiled extension resolves per architecture at install time. Python wheels ship as separate manylinux builds per architecture and an aarch64 wheel may be missing where an x86-64 wheel exists, which sends pip to a source build that needs system headers (pypa/manylinux). Node packages with prebuilt binaries fall back to a node-gyp compile when no aarch64 prebuild is published (nodejs/node-gyp). Under emulation your install step often resolves the x86-64 artifact and never exercises the aarch64 path, so the failure lands in production instead of in the test job.
A third, quieter class sits alongside them: hardware probes. Core counts, CPU feature flags, and page size read from the host under emulation, so any code that sizes a thread pool or picks a SIMD path from those values takes an untested branch.
Picking the machine for the test job
Integration jobs share one runner between the harness and the service containers, so size for both. A workable rule is one vCPU per parallel test worker plus one per service, then round up to the next label. Every rate below comes from the cloud runners documentation, checked on 2026-08-13.
| Runner label | vCPU | RAM | Per minute | Fits |
|---|---|---|---|---|
| warp-ubuntu-latest-arm64-2x | 2 | 8 GB | $0.003 | one service, serial suite |
| warp-ubuntu-latest-arm64-4x | 4 | 16 GB | $0.006 | one database service, two workers |
| warp-ubuntu-latest-arm64-8x | 8 | 32 GB | $0.012 | database plus cache or queue, four workers |
| warp-ubuntu-latest-arm64-16x | 16 | 64 GB | $0.024 | a compose-sized topology, eight workers |
| warp-ubuntu-latest-arm64-32x | 32 | 128 GB | $0.048 | large fixtures, sixteen workers |
Storage is 150GB SSD on every size and is deleted when the runner terminates, so database dumps and coverage output go to an artifact or a cache. The latest labels track Ubuntu 24.04; pin warp-ubuntu-2404-arm64-8x or warp-ubuntu-2604-arm64-8x when you want the release fixed.
What the test job costs
warp-ubuntu-latest-arm64-8x costs $0.012 per minute for 8 vCPU and 32 GB, against $0.014 per minute for the 8-core Linux ARM64 larger runner at the same 8 vCPU and 32 GB shape: 14 percent lower list price (GitHub billing reference, checked on 2026-08-13).
A suite that runs on every pull request and every merge, 1,200 times a month at 8 minutes a run, is 9,600 minutes:
warp-ubuntu-latest-arm64-8xat $0.012: $115.20 per month.- GitHub 8-core Linux ARM64 larger runner at $0.014: $134.40 per month.
- Difference: $19.20 a month, $230.40 over twelve months.
The same 9,600 minutes on warp-ubuntu-latest-x64-8x at $0.016 per minute is $153.60, so moving the job to ARM64 at identical vCPU and RAM changes the line by $38.40 a month. Per-size arithmetic for every label is on the pricing page.
Practical setup notes
Service images need an aarch64 manifest. The official Postgres, MySQL, Redis, and RabbitMQ images publish linux/arm64 alongside linux/amd64, and the supported architectures for each official image are listed in its library file (docker-library/official-images). For any other tag, run docker manifest inspect <image>:<tag> once and check for linux/arm64 before you pin it. Containers started from inside the job, including Testcontainers suites, follow the same rule.
Ubuntu 24.04 ARM64 runners set the work dir to /runner/_work, which differs from GitHub's /home/runner/work/, so replace hardcoded paths with ${{ github.workspace }} and $GITHUB_WORKSPACE before the first run (cloud runners documentation). Jobs that need /dev/kvm, such as Android emulator tests, stay on an x86-64 runner with the nested-virtualization.enabled=true label.
Two parts of the wider product surface earn their keep on a long integration job. Snapshot runners capture a runner mid-workflow and boot later jobs from that snapshot, and they support Linux ARM64, which suits suites that spend minutes seeding a database before the first assertion (snapshot runners documentation). The Action Debugger opens a shell on the running job, which is the fastest route through an aarch64-only failure you cannot reproduce locally.
Related Questions
Do service containers work on ARM64 runners?
Yes, as long as the image publishes a linux/arm64 manifest. The official Postgres, MySQL, Redis, and RabbitMQ images do. Run docker manifest inspect on any other tag before you pin it, because a single-arch tag fails to start on an aarch64 runner. The service containers guide covers health checks and port mapping in more depth.
Which ARM64 size should an integration suite run on?
Budget one vCPU for each parallel test worker plus one for each service container, then pick the next size up. A suite with four workers and two services fits warp-ubuntu-latest-arm64-8x at $0.012 per minute. The full ladder from 2 vCPU to 32 vCPU is in the Linux ARM64 runner catalog.
Can I keep unit tests on x64 and run integration tests on ARM64?
Yes. runs-on is set per job, so one workflow can send the unit test job to warp-ubuntu-latest-x64-4x and the integration job to warp-ubuntu-latest-arm64-8x, with artifacts passed between them. Running ARM64 and x64 jobs in one workflow walks through the matrix and needs shapes.
Start with the labels in the Linux ARM64 runner catalog, price the change against your own job minutes on the pricing page, and keep dependency restore fast with the caching documentation.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.