Skip to content

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

WorkloadNative arm64QEMU on x64Penalty
Static site, copy only18s54s
Node app, npm ci1m 40s9m 20s5.6×
Go binary50s11m 10s13×
Rust release build3m 10s1h 52m35×
C++ with CMake4m 05s2h 41m39×

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.

yaml
strategy:
matrix:
include:
- platform: linux/amd64
runner: runnerhut-8vcpu-ubuntu-2404
- platform: linux/arm64
runner: runnerhut-8vcpu-ubuntu-2404-arm
runs-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.