benchmarks
Why your QEMU arm64 build is 40× slower
A measured breakdown of emulated versus native arm64 Docker builds across five real workloads, and why the penalty varies so much.
Tomás Rivera · Performance engineering · 2026-06-11 · 8 min read
Adding linux/arm64 to a Docker build is one line. The consequence is sometimes a 3× slowdown and sometimes a 40× one, and the difference confuses people. It is not random.
The mechanism
QEMU in user-mode emulation translates arm64 instructions into x64 instructions at runtime. Translation has fixed overhead per instruction. So the penalty scales with how many instructions your build executes — not with how long it takes, and not with image size.
A build that copies files and runs apt-get executes relatively few instructions. A build that runs a compiler executes an enormous number.
Measured
| Workload | Native arm64 | QEMU on x64 | Penalty |
|---|---|---|---|
| Static site, copy only | 18s | 54s | 3× |
| Node app, npm ci | 1m 40s | 9m 20s | 5.6× |
| Go binary | 50s | 11m 10s | 13× |
| Rust release build | 3m 10s | 1h 52m | 35× |
| C++ with CMake | 4m 05s | 2h 41m | 39× |
The pattern is clean: the more your build compiles, the worse emulation gets. If your Dockerfile invokes a compiler, QEMU is not a viable strategy.
The fix
Build each architecture on its own hardware and merge the digests into a manifest list. It is more YAML than a single platforms line, and it is the difference between three minutes and two hours.
strategy: matrix: include: - platform: linux/amd64 runner: runnerhut-8vcpu-ubuntu-2404 - platform: linux/arm64 runner: runnerhut-8vcpu-ubuntu-2404-armruns-on: ${{ matrix.runner }}Your next build could be twice as fast, at half the price
Start free. Migrating away is the same one line, and we publish that diff too.