The Test Setup

To make these benchmarks meaningful, the hardware specifics matter:

  • x86 baseline: AWS m7i.xlarge, 4 vCPUs (Intel Sapphire Rapids 4th Gen), 16GB RAM, EBS gp3
  • ARM64 target: r8g.xlarge, 4 vCPUs (Graviton4, Neoverse V2), 32GB RAM, EBS gp3
  • Region: us-east-1
  • OS: Amazon Linux 2023, kernel 6.1
  • Testing: Each benchmark ran 10 times, numbers are median with p5/p95 bounds

Yes, the r8g has more RAM. Instance families should be chosen based on actual workload needs, not synthetic benchmark symmetry. In production, the right instance fits the application, not a comparison chart.

Node.js (v22.x): The Surprising Middle Ground

A representative Node.js service for benchmarking is an API gateway, Express.js, lots of JSON parsing, JWT validation, rate limiting, response transformation. Classic I/O-bound with bursts of CPU for crypto and JSON.

HTTP Throughput (wrk, 100 concurrent connections, 60s)

Metric m7i.xlarge (x86) r8g.xlarge (ARM64) Delta
Requests/sec 14,230 15,870 +11.5%
Avg latency 6.89ms 6.21ms -9.9%
p99 latency 18.4ms 14.7ms -20.1%
CPU usage (avg) 72% 64% -11.1%

The p99 improvement is the most notable result. Graviton4's more predictable core performance (no turbo boost variance) gives you tighter latency distributions. In a microservices architecture, that tail latency compounding effect is huge.

JSON Serialization (1MB payload, 10K iterations)

// Benchmark: JSON.parse + transform + JSON.stringify
const payload = generateLargeJSON(1_000_000); // ~1MB
const serialized = JSON.stringify(payload);

console.time('json-roundtrip');
for (let i = 0; i < 10_000; i++) {
 const parsed = JSON.parse(serialized);
 parsed.metadata.processedAt = Date.now();
 parsed.items = parsed.items.filter(i => i.active);
 JSON.stringify(parsed);
}
console.timeEnd('json-roundtrip');
Runtime x86 ARM64 Delta
Node.js 22 34.2s 31.8s -7.0%
Node.js 22 + --max-semi-space-size=128 28.1s 25.4s -9.6%

V8's JSON parser benefits from Graviton4's improved memory bandwidth. The semi-space flag matters more on ARM64 because the GC pressure profile is different, Graviton4's larger L2 cache (2MB per core vs 1.25MB on Sapphire Rapids) keeps more young-gen objects hot.

The Node.js Gotcha: Native Modules

Several common npm dependencies lack pre-built ARM64 binaries:

  • sharp, had ARM64 binaries, worked perfectly
  • bcrypt, had ARM64 binaries, worked perfectly
  • cpu-features, no ARM64 binary, fell back to WASM, 3x slower
  • re2, no ARM64 binary, build from source required python3 and g++ in the container
  • canvas, no ARM64 binary, required manual compilation with system deps

The cpu-features fallback is invisible, no warnings, no errors, just silently slower. It often surfaces only when JWT validation benchmarks come in worse than expected on ARM64, leading to the discovery that the WASM fallback is handling the feature detection internally.

Always check: npm ls --all | xargs -I {} npm info {} cpu

Go (1.23): Where ARM64 Really Shines

Go on ARM64 is a different story. The Go compiler has had first-class ARM64 support since Go 1.16, and the runtime is heavily optimized for it.

HTTP API Benchmark (net/http, JSON CRUD, PostgreSQL backend)

Metric m7i.xlarge (x86) r8g.xlarge (ARM64) Delta
Requests/sec 28,450 34,120 +19.9%
Avg latency 3.45ms 2.88ms -16.5%
p99 latency 11.2ms 8.1ms -27.7%
Allocs/op 42 42 0%
GC pause (p99) 0.8ms 0.6ms -25%

Twenty percent more throughput. That's not a rounding error. And the GC pauses are shorter because Graviton4's memory subsystem is genuinely better, higher bandwidth, lower latency to L2/L3.

The GOARM64 Flag: Free Performance

This is the tip most people miss. Go 1.23 introduced GOARM64 for fine-grained ARM64 feature targeting:

# Default build (conservative, runs on any ARM64)
GOOS=linux GOARCH=arm64 go build -o service ./cmd/service

# Graviton4-optimized build (uses ARMv9 features including SVE2)
GOOS=linux GOARCH=arm64 GOARM64=v9.0 go build -o service ./cmd/service

The difference on a compute-heavy operation (a recommendation engine, heavy on float64 math):

Build Throughput Latency p50
Default ARM64 12,340 ops/s 4.1ms
GOARM64=v9.0 14,780 ops/s 3.4ms

That's 19.8% more throughput from a single environment variable. The v9.0 flag enables SVE2 (Scalable Vector Extension 2) instructions, which Graviton4 supports. For workloads with floating-point math, string processing, or cryptographic operations, this is free performance.

Compiler Flags with Measurable Impact

Beyond GOARM64, these flags showed measurable differences on ARM64:

# PGO (Profile-Guided Optimization), works on ARM64 since Go 1.21
go build -pgo=default.pgo -o service ./cmd/service

# Results with PGO on ARM64:
# Without PGO: 34,120 req/s
# With PGO: 37,230 req/s (+9.1%)

PGO on ARM64 is slightly more effective than on x86 because the branch predictor on Graviton4 benefits more from the profile data. Collect a CPU profile from production (runtime/pprof), save it as default.pgo in your main package directory, and rebuild. It's genuinely free performance.

Rust (1.82): The Beast on ARM64

Rust on Graviton4 is where things get exciting. LLVM's ARM64 backend is exceptionally mature, and Rust gives you the control to exploit it.

Image Processing Service Benchmark

An image processing service doing real-time resizing and format conversion (WebP, AVIF) provides a SIMD-heavy, memory-bandwidth-intensive benchmark.

// Target Graviton4 specifically
// .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
rustflags = ["-C", "target-cpu=neoverse-v2"]
Operation x86 (AVX2) ARM64 (default) ARM64 (neoverse-v2) Delta vs x86
JPEG decode (4K) 12.3ms 14.1ms 11.2ms -8.9%
WebP encode 28.7ms 31.2ms 24.8ms -13.6%
AVIF encode 89.4ms 97.1ms 78.3ms -12.4%
Resize 4K→1080p 8.4ms 9.1ms 7.2ms -14.3%

Look at the difference between default ARM64 and target-cpu=neoverse-v2. Without the target CPU flag, ARM64 is actually slower than x86 for SIMD-heavy workloads because the compiler generates conservative NEON code. With the flag, it uses SVE2 with wider vectors and the performance flips.

NEON vs SVE2: What You Need to Know

ARM64 has two SIMD instruction sets:

  • NEON: Fixed 128-bit vectors, available on all ARM64. Equivalent to SSE4.2.
  • SVE2: Scalable vectors (128-512 bit, Graviton4 uses 128-bit but with additional instructions), available on ARMv9. Equivalent to AVX2/AVX-512 in capability, though not in raw vector width.

If you're using SIMD intrinsics directly in Rust:

// NEON (works everywhere)
#[cfg(target_arch = "aarch64")]
use std::arch::aarch64::*;

unsafe fn sum_neon(data: &[f32]) -> f32 {
 let mut acc = vdupq_n_f32(0.0);
 for chunk in data.chunks_exact(4) {
 let v = vld1q_f32(chunk.as_ptr());
 acc = vaddq_f32(acc, v);
 }
 vaddvq_f32(acc)
}

For most Rust code, you don't need intrinsics. The compiler auto-vectorizes when you set the target CPU correctly. But if you're porting x86 SIMD code, be aware that NEON and AVX have different semantics for some operations (notably: no direct equivalent of _mm_shuffle_ps, you need vqtbl1q_u8 or similar).

Cost Analysis: Four Months in Production

After 4 months in production, here are the real numbers:

Compute Costs (14 services, us-east-1)

x86 (m7i family) ARM64 (r8g family) Savings
On-Demand monthly $4,847 $3,878 -20.0%
Reserved 1yr monthly $3,105 $2,484 -20.0%
Spot monthly (avg) $1,939 $1,242 -35.9%

The Spot savings are the standout. AWS has significantly more ARM64 Spot capacity (fewer people using it), so interruption rates are lower and prices are more stable. Over a 4-month period, average Spot interruption rates on ARM64 measured 2.1% vs 5.8% on x86.

Total Infrastructure Savings

Combining the performance improvement (which allows downsizing some instances) with the lower per-vCPU cost:

  • Before (x86, mixed On-Demand/Reserved): $3,740/month
  • After (ARM64, mixed Reserved/Spot): $2,180/month
  • Net savings: $1,560/month (-41.7%)

That 41.7% is not just the ARM64 price difference, it includes instance rightsizing, since the same workloads need fewer vCPUs on Graviton4.

Migration Checklist

A condensed migration checklist based on production experience:

  1. Container base images: Switch to multi-arch or explicit arm64v8/ images. FROM node:22-slim already pulls the right arch automatically.

  2. CI/CD: Add ARM64 runners. GitHub Actions has runs-on: ubuntu-24.04-arm now. If self-hosted, Graviton3 c7g instances make excellent CI runners.

  3. Native dependencies audit: Run your test suite on ARM64 first. Check for WASM fallbacks, missing binaries, compilation requirements.

  4. Compiler flags: Set target CPU for compiled languages. GOARM64=v9.0 for Go, target-cpu=neoverse-v2 for Rust, -march=armv9-a for C/C++.

  5. Monitoring: Watch for performance regressions in specific operations. Crypto operations are commonly slower initially due to missing ARM64 OpenSSL engine configuration.

  6. Gradual rollout: Use weighted target groups in your load balancer. Start with 10% traffic to ARM64, validate metrics, increase.

Benchmark Limitations and Caveats

Every benchmark post (including this one) is inherently misleading because it measures specific workloads on specific configurations. Results will vary. Some areas are deliberately excluded because they would be misleading:

  • Memory-bound workloads: The r8g has different RAM characteristics than m7i. Direct comparison is apples-to-oranges.
  • Single-threaded peak performance: x86 turbo boost can hit 3.6GHz+. Graviton4 runs at a fixed 3.0GHz. For burst single-thread, x86 wins.
  • AVX-512 workloads: If your code uses AVX-512 (ML inference, scientific computing), x86 has a genuine advantage. Graviton4's SVE2 is not a direct equivalent.

The right approach: run your actual production workload on both architectures with real traffic. Synthetic benchmarks tell you direction, not magnitude.

Is It Worth It?

The 41% cost reduction alone justifies the roughly two weeks of migration work. But the less quantifiable benefit is operational: Graviton4's fixed-frequency cores deliver more predictable performance, which means autoscaling is more responsive and capacity planning is simpler.

For AWS-based workloads, Graviton4 evaluation is increasingly table stakes. The ARM64 ecosystem is mature enough that the migration is boring, and in infrastructure, boring is exactly the goal.