Back to Blog
AWSGravitoncontainerreduce_costs

Graviton and the honest arm64 migration: where the 20-40% is real and where it is not

Why AWS Graviton wins on price/performance, what actually differs at the ISA and memory level, the migration friction nobody warns you about, and the workloads where ARM quietly does nothing.

JohannaMay 6, 20265 min read

Graviton is the rare cost optimization that is also a performance story, which is exactly why it gets oversold. AWS's own headline — up to 40% better price/performance versus comparable x86 — is achievable, but it is an aggregate claim across a benchmark suite, and treating it as a per-workload guarantee is how migrations end in disappointment. The useful version of this decision is knowing what actually changes when you move from m6i to m7g, what breaks, and which of your workloads will shrug at the whole exercise.

Where the number comes from

Graviton instances are built on Arm Neoverse cores that AWS designs and fabricates itself, which removes a licensing and margin layer that x86 vendors cannot. Two effects compound:

  • The hourly rate is lower. For an equivalently sized instance, the Graviton variant typically lists around 20% cheaper than its Intel or AMD counterpart — c7g against c7i, m8g against m7i.
  • Per-core throughput is competitive to better on many server workloads, so you often need no more cores to match performance.

Multiply a ~20% lower rate by equal-or-better throughput and you land in the 20-40% price/performance band. The generational lineage matters here: Graviton2 (the 6g families, Neoverse N1) was the "as fast, much cheaper" step; Graviton3 (7g, Neoverse V1) added DDR5, higher memory bandwidth, SVE, and bfloat16; Graviton4 (8g, Neoverse V2) pushed core counts, memory channels, and bandwidth further. A workload that was memory-bandwidth-limited on x86 can see a larger jump than the rate cut alone, because Graviton3 and 4 feed the cores faster.

What is genuinely different under the hood

Three differences are worth understanding rather than hand-waving.

The instruction set is aarch64, not x86-64. Every binary in your image must exist as arm64. This is a rebuild, not a recompile flag, and it is where most of the real work lives (below).

The memory model is weaker. ARM uses a relaxed memory-ordering model; x86 uses the stronger TSO (total store order). Correctly synchronized code — anything using real locks, atomics, or your language's memory-model-respecting primitives — behaves identically. But code with latent data races that happened to work on x86 because TSO papered over them can surface real bugs on ARM. This is not Graviton being buggy; it is Graviton being an honest implementation of a weaker model that exposes concurrency mistakes x86 tolerated. Budget for it if you run old C/C++ or hand-rolled lock-free code.

SVE and vectorization. Graviton3 introduced SVE (Scalable Vector Extension) and Graviton4 continues it, but SIMD gains are not automatic. Software has to be compiled with the right target and, ideally, do runtime feature dispatch. A numerical library shipped as a generic aarch64 build may leave SVE on the table; one built with -mcpu=neoverse-v1 (or v2) and the right vectorization will not. If your hot path is SIMD-heavy, verify the build flags rather than assuming.

The migration friction that is actually real

The concept is trivial; the yak-shaving is not. In rough order of how often it bites:

Multi-arch container builds. Your image and every base layer need an arm64 variant, published as a manifest list (an OCI image index) so the same tag resolves to the right architecture per node. The tooling is docker buildx build --platform linux/amd64,linux/arm64 --push, and ECR stores the multi-arch manifest fine. The trap is how you build the arm64 layer. Cross-building via QEMU emulation on an x86 runner works but is painfully slow for anything compilation-heavy — often 5-10x. Build arm64 natively instead: a Graviton CI runner, an ARM_CONTAINER CodeBuild fleet, or arm64 GitHub runners. Native builders turn a 40-minute emulated build back into a few minutes.

Native dependencies and prebuilt binaries. Interpreted languages are only as portable as their compiled extensions:

  • Python needs manylinux2014_aarch64 (or newer) wheels for anything with C extensions — numpy, pandas, grpcio, cryptography. Most popular packages ship them now; the long tail does not, and pip will silently fall back to building from source, which needs a toolchain in your image.
  • Node.js native addons (node-gyp, N-API modules) need arm64 prebuilds. Packages using prebuild/prebuildify usually ship them; those that do not will node-gyp rebuild at install, again requiring build tools present.
  • Go and Rust are the easy case: GOARCH=arm64 and the aarch64-unknown-linux-gnu target cross-compile cleanly with no emulation.

x86-only binaries. The genuine blockers are third-party or proprietary bits with no arm64 build: certain vendor agents, older commercial database clients, some closed-source ML kernels tuned for AVX-512. If one of these sits in your critical path and the vendor has no aarch64 release, that service simply cannot move until they ship one.

Base image availability. Official images (debian, alpine, ubuntu, node, python, the JDKs, Postgres) are multi-arch and just work. The risk is a third-party or internal base image published amd64-only; the multi-arch pull then fails on arm64 nodes. Audit your base layers before flipping node types.

Benchmark honestly: price/performance, not raw speed

The single most common mistake is comparing single-thread wall-clock and declaring ARM slower. That is the wrong axis. The question is throughput per dollar at your real concurrency. Run the actual workload — your service under representative load, your batch job on real data — on c7g and on c7i, measure requests per second (or jobs per hour) sustained, and divide by the instance's hourly cost. Graviton frequently trails on a single core while winning decisively on throughput-per-dollar because it gives you more, cheaper cores and better memory bandwidth. Also measure p99 latency, not just averages: a memory-bandwidth improvement can tighten tails in ways an average hides.

Where ARM quietly does not help

Being straight about the null cases keeps you credible:

  • You are not CPU-bound. If a service spends its time waiting on a database, an external API, or the network, a faster or cheaper core changes little on performance — though you still pocket the ~20% rate cut, so it is a cost win with no performance story. Say that honestly rather than claiming a speedup.
  • Per-core-licensed commercial software. If you pay a vendor per core, and Graviton needs the same or more cores to hit your throughput, the license cost can swamp the infrastructure savings. Do that math before migrating a licensed engine.
  • AVX-512-heavy numerical or media code with hand-tuned x86 assembly. Some video encoders, crypto, and HPC kernels have years of x86 SIMD optimization that a generic aarch64 build will not match. Benchmark these specifically; occasionally x86 genuinely wins.
  • GPU- or memory-price-dominated workloads. If your bill is mostly GPU hours or provisioned memory, the CPU family is a rounding error and Graviton is not the lever to pull.

For the broad middle — stateless web and API tiers, JVM and Go and Node services, most containers, caches, and throughput batch — Graviton is close to a free 20-40%, and the friction is a finite, one-time engineering cost you pay in the build pipeline. The discipline is to migrate the workloads that benefit, measure them on price/performance rather than raw speed, and be honest about the ones where ARM changes nothing but the invoice.