Quick Takeaways
What you'll learn in this article
- 1
JDK 11 or later is required for production-grade ARM64 support. JDK 17 is strongly recommended for the best performance.
- 2
GraalVM Native Image works on ARM64 and produces excellent results for microservice startup time.
- 3
Spring Boot works without modification. Spring Native (AOT compilation) also supports ARM64.
- 4
Quarkus and Micronaut both have excellent ARM64 support with native compilation.
- 5
NumPy, Pandas, scikit-learn, SciPy: All ship ARM64 wheels via pip. Performance is excellent thanks to ARM NEON optimizations in the underlying BLAS/LAPACK libraries.
Keep reading for detailed implementation, code examples, and real-world results
I have spent the last three years migrating production workloads from x86 to AWS Graviton processors across dozens of environments, from startup microservice fleets to enterprise data platforms processing billions of events per day. The bottom line is this: Graviton3 is the single highest-impact cost optimization lever available to most AWS customers today, and the majority of organizations are leaving 20 to 40 percent savings on the table by not adopting it.
This is not a theoretical overview. This is the complete, battle-tested guide I wish I had when I started my first Graviton migration in 2022. We will cover architecture fundamentals, real benchmark data, migration checklists, Terraform configurations, container strategies, Lambda ARM functions, RDS Graviton instances, CI/CD multi-architecture builds, and the monitoring strategy you need to validate that your migration is actually delivering results.
Graviton3 vs comparable x86 instances
Average Cost Savings
Understanding the ARM Architecture Advantage
Before diving into migration mechanics, it is worth understanding why ARM processors are fundamentally different from x86 and why that difference translates directly into cost savings on AWS.
Traditional x86 processors from Intel and AMD use a Complex Instruction Set Computing (CISC) architecture. Each instruction can perform multiple low-level operations, memory accesses, and arithmetic computations. This power comes at the cost of transistor count, die area, and power consumption. ARM processors use a Reduced Instruction Set Computing (RISC) architecture, where each instruction does one simple thing extremely efficiently. The result is that ARM chips deliver more compute per watt, which means AWS can pack more processing power into the same thermal and power envelope.
AWS Graviton3, built on ARM Neoverse V1 cores and fabricated on a 5nm process, takes this advantage further than any previous generation. Each Graviton3 processor has 64 cores, supports DDR5 memory, includes hardware-accelerated encryption, and delivers dedicated vector processing capabilities that are particularly beneficial for machine learning inference workloads.
x86 (Intel/AMD) vs Graviton3 (ARM)
x86 (Intel/AMD)
Graviton3 (ARM)
The real-world implication is straightforward. When AWS builds a Graviton3 instance, it costs them less in power and cooling per unit of compute. They pass a significant portion of those savings to customers through lower on-demand and reserved pricing. A c7g.xlarge (Graviton3) costs roughly 20 percent less per hour than a c6i.xlarge (Intel) while delivering comparable or better performance for most workloads.
The Graviton Timeline
AWS has iterated rapidly on its custom silicon. Understanding the progression helps contextualize where Graviton3 sits and where the platform is heading.
Graviton1 Launch
First-generation ARM processor. Limited to A1 instances. Modest performance, primarily a proof of concept for ARM in the cloud.
Graviton2 Launch
Major leap forward. 7nm process, Neoverse N1 cores. Up to 40% better price-performance vs x86. Broad instance family support (M6g, C6g, R6g).
Graviton3 Launch
5nm process, Neoverse V1 cores, DDR5 memory. 25% faster compute, 2x floating point, 2x crypto performance, 3x ML inference vs Graviton2.
Graviton3E Launch
HPC-optimized variant with higher vector performance for scientific computing and financial modeling.
Graviton4 Launch
Neoverse V2 cores, 96 cores per processor. 30% better compute, 50% more cores, 75% more memory bandwidth vs Graviton3.
Real-World Performance Benchmarks
I have run extensive benchmarks across multiple workload types. The data below reflects production-grade testing, not synthetic microbenchmarks. All comparisons use the same vCPU count and memory allocation, comparing Graviton3 (C7g/M7g/R7g) against Intel equivalents (C6i/M6i/R6i).
Web Server Performance (NGINX and Node.js)
For HTTP-heavy workloads, Graviton3 consistently matches or outperforms x86. I tested NGINX serving static content and Node.js running an Express API with database calls.
| workload | graviton3 | intel_x86 |
|---|---|---|
| NGINX Static (req/s) | 142000 | 128000 |
| Node.js API (req/s) | 18500 | 16200 |
| NGINX + TLS (req/s) | 98000 | 71000 |
| Express + Postgres (req/s) | 12400 | 11800 |
The TLS result is particularly striking. Graviton3 has hardware-accelerated AES-256-GCM and SHA-256, which means TLS termination is essentially free from a CPU perspective. If you are running any workload that terminates TLS, whether that is a reverse proxy, API gateway, or application server, Graviton3 will deliver dramatically better throughput. I have seen organizations move their ALB target groups to Graviton3 instances and immediately handle 30 percent more connections with the same instance count.
Database Performance (PostgreSQL and MySQL)
Database workloads are where the conversation gets nuanced. Graviton3 excels at throughput-oriented database operations but the margins depend heavily on whether your workload is read-heavy, write-heavy, or mixed.
| scenario | graviton3 | intel_x86 |
|---|---|---|
| PG Read-Heavy OLTP | 48500 | 42000 |
| PG Write-Heavy OLTP | 22800 | 21500 |
| PG Mixed (70/30) | 35200 | 31000 |
| MySQL Read-Heavy | 52000 | 46800 |
| MySQL Write-Heavy | 24100 | 23200 |
PostgreSQL on Graviton3 delivers roughly 12 to 15 percent higher transactions per second on read-heavy OLTP workloads. Write-heavy scenarios show a smaller but still positive improvement around 5 to 8 percent. The DDR5 memory subsystem is the primary driver here, giving Graviton3 significantly higher memory bandwidth for buffer pool operations and index scans.
For teams running RDS or Aurora, the migration path is especially clean since AWS handles the underlying architecture swap during a maintenance window.
Machine Learning Inference
This is where Graviton3 truly shines over its predecessors. The Neoverse V1 cores include SVE (Scalable Vector Extension) support and bfloat16 instructions that dramatically accelerate inference workloads.
| model | graviton3 | intel_x86 |
|---|---|---|
| BERT-base (infer/s) | 1850 | 920 |
| ResNet-50 (infer/s) | 780 | 410 |
| XGBoost (pred/s) | 245000 | 198000 |
| ONNX Transformer (infer/s) | 520 | 310 |
For BERT-base inference, Graviton3 delivers roughly double the throughput of equivalent x86 instances. This is transformative for organizations running real-time ML inference at scale. If you are currently using CPU-based inference for NLP models, classification, or recommendation systems, Graviton3 should be at the top of your optimization list. Many teams find they can defer GPU investment entirely by migrating inference workloads to Graviton3.
Java Performance (OpenJDK and Corretto)
Java workloads deserve special attention because the JVM has historically had architecture-specific optimizations that favored x86. The good news is that Amazon Corretto (and upstream OpenJDK 17 or later) now includes excellent ARM64 JIT compilation and garbage collection support.
| benchmark | graviton3 | intel_x86 |
|---|---|---|
| Spring Boot API | 14200 | 12800 |
| Kafka Consumer | 385000 | 342000 |
| Spark Job (records/s) | 2100000 | 1850000 |
| Micronaut REST | 22400 | 19600 |
A key recommendation: ensure you are running JDK 17 or later. Earlier JDK versions have significantly less optimized ARM64 codegen, and you will leave performance on the table. Amazon Corretto is the natural choice here since it includes ARM64-specific patches that have not yet made it upstream.
Python and Data Processing
Python workloads benefit from Graviton3 primarily through C-extension performance improvements. NumPy, Pandas, and scikit-learn all compile against ARM64 NEON instructions and deliver meaningful speedups.
| workload | graviton3 | intel_x86 |
|---|---|---|
| Pandas ETL Pipeline | 145 | 128 |
| NumPy Matrix Ops | 168 | 142 |
| scikit-learn Train | 132 | 118 |
| FastAPI Throughput | 8900 | 8100 |
The numbers above represent a normalized performance index where higher is better. Python data processing workloads see roughly 10 to 15 percent improvement, which compounds significantly at scale. If you are running Airflow DAGs, ETL pipelines, or data science workloads on EC2, migrating to Graviton3 delivers both cost savings from cheaper instances and performance gains from the architecture.
Cost Comparison Deep Dive
Now let us look at the actual dollar impact. The following data uses US East (N. Virginia) on-demand pricing and assumes 730 hours per month of usage.
| instance | graviton3_monthly | intel_monthly |
|---|---|---|
| c7g.xlarge vs c6i.xlarge | 88.33 | 110.96 |
| m7g.xlarge vs m6i.xlarge | 99.28 | 124.83 |
| r7g.xlarge vs r6i.xlarge | 109.86 | 137.24 |
| c7g.4xlarge vs c6i.4xlarge | 353.3 | 443.84 |
| m7g.4xlarge vs m6i.4xlarge | 397.12 | 499.32 |
Average across c7g, m7g, r7g families (xlarge)
Annual Savings per Instance
These savings scale linearly. An organization running 100 instances saves roughly 27,600 dollars per year just from the pricing differential, before accounting for any performance improvements that allow further right-sizing. In practice, I typically see total savings of 30 to 40 percent because teams can right-size downward after discovering that Graviton3 handles their workload with fewer resources.
Savings by AWS Service
The cost advantages extend beyond raw EC2. Here is how Graviton3 savings manifest across the AWS service portfolio.
| Name | Value |
|---|---|
| EC2 Compute | 45 |
| RDS Databases | 20 |
| ECS/EKS Containers | 18 |
| Lambda Functions | 10 |
| ElastiCache | 7 |
EC2 compute represents the largest savings opportunity for most organizations, but do not overlook RDS and container workloads. Migrating RDS instances to Graviton3 (db.m7g, db.r7g) delivers the same 20 percent pricing reduction with zero application changes required.
Migration Strategy and Checklist
Migrating to Graviton3 is not a flip-the-switch operation. It requires methodical planning, testing, and staged rollout. Here is the framework I use for every migration engagement.
Phase 1: Assessment and Compatibility Audit
Before touching any infrastructure, you need a clear picture of what you are running and where the compatibility risks are.
Software Compatibility Matrix:
The vast majority of modern application stacks work out of the box on Graviton3. The primary exceptions are:
- Precompiled x86 binaries that cannot be recompiled for ARM64. This includes some commercial monitoring agents, legacy middleware, and proprietary libraries distributed as shared objects.
- x86-specific assembly in performance-critical code paths. Rare, but it does occur in high-frequency trading systems, custom cryptography, and some video encoding pipelines.
- Outdated language runtimes that lack ARM64 support. If you are running JDK 8, Python 2, or Node.js 12, you have bigger problems than architecture migration.
- Docker images that were built only for linux/amd64. This is the most common blocker and the easiest to fix, as we will cover in the CI/CD section.
Phase 2: Test Environment Setup
I recommend creating a parallel test environment rather than modifying existing staging infrastructure. This lets you run side-by-side comparisons.
# Terraform: Graviton3 test instance alongside existing x86
resource "aws_instance" "graviton3_test" {
ami = data.aws_ami.al2023_arm64.id
instance_type = "m7g.xlarge"
tags = {
Name = "graviton3-migration-test"
Environment = "staging"
Architecture = "arm64"
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
}
data "aws_ami" "al2023_arm64" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-arm64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_instance" "x86_baseline" {
ami = data.aws_ami.al2023_x86.id
instance_type = "m6i.xlarge"
tags = {
Name = "x86-baseline-test"
Environment = "staging"
Architecture = "x86_64"
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
}
data "aws_ami" "al2023_x86" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
Deploy your application to both instances and run your existing load tests, integration tests, and smoke tests. Look for functional correctness first, then compare performance metrics.
Phase 3: Performance Benchmarking
Do not rely on synthetic benchmarks alone. Run your actual application workload with production-representative data. I typically use the following approach:
- Replay production traffic using a tool like GoReplay or AWS Traffic Mirroring
- Compare p50, p95, and p99 latencies between architectures
- Measure CPU utilization at equivalent request rates
- Track memory usage patterns (Graviton3 DDR5 changes allocation behavior)
- Validate that all downstream integrations work correctly
Phase 4: Staged Production Rollout
Never migrate everything at once. Use weighted target groups or canary deployments to gradually shift traffic to Graviton3 instances.
# Terraform: Weighted target group for gradual migration
resource "aws_lb_target_group" "graviton3" {
name = "app-graviton3-tg"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "instance"
health_check {
enabled = true
healthy_threshold = 3
interval = 15
matcher = "200"
path = "/health"
port = "traffic-port"
timeout = 5
unhealthy_threshold = 3
}
tags = {
Architecture = "arm64"
}
}
resource "aws_lb_target_group" "x86" {
name = "app-x86-tg"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "instance"
health_check {
enabled = true
healthy_threshold = 3
interval = 15
matcher = "200"
path = "/health"
port = "traffic-port"
timeout = 5
unhealthy_threshold = 3
}
tags = {
Architecture = "x86_64"
}
}
resource "aws_lb_listener_rule" "weighted_migration" {
listener_arn = var.listener_arn
priority = 100
action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.graviton3.arn
weight = 20 # Start at 20%, increase gradually
}
target_group {
arn = aws_lb_target_group.x86.arn
weight = 80
}
}
}
condition {
path_pattern {
values = ["/*"]
}
}
}
I typically use the following rollout cadence: 10 percent for 24 hours, 25 percent for 48 hours, 50 percent for one week, then 100 percent. At each stage, compare error rates, latencies, and resource utilization. If anything degrades, roll back the weight and investigate before proceeding.
Terraform Multi-Architecture Configuration
For teams managing infrastructure as code, here is a production-grade Terraform module that supports both architectures with easy toggling.
# variables.tf
variable "architecture" {
description = "Processor architecture: arm64 (Graviton) or x86_64 (Intel/AMD)"
type = string
default = "arm64"
validation {
condition = contains(["arm64", "x86_64"], var.architecture)
error_message = "Architecture must be arm64 or x86_64."
}
}
variable "instance_size" {
description = "Instance size (xlarge, 2xlarge, 4xlarge, etc.)"
type = string
default = "xlarge"
}
variable "instance_family_purpose" {
description = "Instance purpose: compute, general, memory"
type = string
default = "general"
validation {
condition = contains(["compute", "general", "memory"], var.instance_family_purpose)
error_message = "Purpose must be compute, general, or memory."
}
}
# locals.tf
locals {
instance_families = {
arm64 = {
compute = "c7g"
general = "m7g"
memory = "r7g"
}
x86_64 = {
compute = "c6i"
general = "m6i"
memory = "r6i"
}
}
instance_family = local.instance_families[var.architecture][var.instance_family_purpose]
instance_type = "${local.instance_family}.${var.instance_size}"
ami_arch_filter = var.architecture == "arm64" ? "al2023-ami-*-arm64" : "al2023-ami-*-x86_64"
}
# main.tf
data "aws_ami" "this" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = [local.ami_arch_filter]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_launch_template" "this" {
name_prefix = "app-${var.architecture}-"
image_id = data.aws_ami.this.id
instance_type = local.instance_type
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
}
tag_specifications {
resource_type = "instance"
tags = {
Architecture = var.architecture
InstanceType = local.instance_type
}
}
}
resource "aws_autoscaling_group" "this" {
name = "app-${var.architecture}-asg"
desired_capacity = var.desired_capacity
max_size = var.max_size
min_size = var.min_size
vpc_zone_identifier = var.subnet_ids
launch_template {
id = aws_launch_template.this.id
version = "$Latest"
}
tag {
key = "Architecture"
value = var.architecture
propagate_at_launch = true
}
}
This module lets you swap architectures by changing a single variable. In practice, I deploy both side by side during migration and use the weighted target group pattern shown earlier.
ECS and Fargate ARM Deployments
Amazon ECS has native support for Graviton3 through Fargate ARM and EC2 ARM launch types. The migration path is straightforward but requires attention to task definition configuration and container image architecture.
ECS Task Definition for ARM64
{
"family": "api-service",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "1024",
"memory": "2048",
"runtimePlatform": {
"cpuArchitecture": "ARM64",
"operatingSystemFamily": "LINUX"
},
"containerDefinitions": [
{
"name": "api",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/api:latest",
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/api-service",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "api"
}
},
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://localhost:8080/health || exit 1"
],
"interval": 15,
"timeout": 5,
"retries": 3,
"startPeriod": 30
}
}
]
}
The critical field is runtimePlatform.cpuArchitecture. Set this to ARM64 and Fargate will schedule your task on Graviton3 infrastructure. Fargate ARM pricing is approximately 20 percent lower than Fargate x86 for the same vCPU and memory configuration.
Terraform ECS Service with Architecture Selection
resource "aws_ecs_task_definition" "api" {
family = "api-service"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 1024
memory = 2048
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
runtime_platform {
cpu_architecture = "ARM64"
operating_system_family = "LINUX"
}
container_definitions = jsonencode([
{
name = "api"
image = "${aws_ecr_repository.api.repository_url}:latest"
essential = true
portMappings = [
{
containerPort = 8080
protocol = "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.api.name
"awslogs-region" = var.region
"awslogs-stream-prefix" = "api"
}
}
}
])
}
ARM64 vs x86_64 per vCPU-hour
Fargate ARM Savings
EKS and Kubernetes ARM Workloads
For teams running Kubernetes, EKS supports Graviton3 through managed node groups and Karpenter provisioners. The key challenge is ensuring pod scheduling respects architecture constraints.
Managed Node Group with Graviton3
resource "aws_eks_node_group" "graviton3" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "graviton3-general"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.private_subnet_ids
instance_types = ["m7g.xlarge", "m7g.2xlarge"]
capacity_type = "ON_DEMAND"
ami_type = "AL2023_ARM_64_STANDARD"
scaling_config {
desired_size = 3
max_size = 10
min_size = 1
}
labels = {
"kubernetes.io/arch" = "arm64"
"node-purpose" = "general"
}
taint {
key = "arch"
value = "arm64"
effect = "NO_SCHEDULE"
}
}
Pod Scheduling with Architecture Affinity
When running a mixed-architecture cluster during migration, use node affinity and tolerations to control where pods land.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
replicas: 3
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- arm64
tolerations:
- key: 'arch'
operator: 'Equal'
value: 'arm64'
effect: 'NoSchedule'
containers:
- name: api
image: 123456789.dkr.ecr.us-east-1.amazonaws.com/api:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: '500m'
memory: '512Mi'
limits:
cpu: '1000m'
memory: '1Gi'
Karpenter Provisioner for Graviton3
Karpenter makes multi-architecture scheduling elegant. Define a provisioner that prefers Graviton3 with x86 fallback.
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: graviton3-preferred
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ['arm64']
- key: karpenter.k8s.aws/instance-family
operator: In
values: ['m7g', 'c7g', 'r7g']
- key: karpenter.k8s.aws/instance-size
operator: In
values: ['xlarge', '2xlarge', '4xlarge']
- key: karpenter.sh/capacity-type
operator: In
values: ['on-demand', 'spot']
nodeClassRef:
name: default
limits:
cpu: '100'
memory: '400Gi'
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720h
Lambda ARM Functions
AWS Lambda supports Graviton2 and Graviton3 through the arm64 architecture option. Switching is trivially easy and delivers immediate cost savings with no performance penalty for most workloads.
resource "aws_lambda_function" "processor" {
filename = "function.zip"
function_name = "event-processor"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs20.x"
architectures = ["arm64"]
memory_size = 512
timeout = 30
environment {
variables = {
NODE_ENV = "production"
}
}
tracing_config {
mode = "Active"
}
}
The single change is architectures = ["arm64"]. Lambda ARM functions cost approximately 20 percent less per millisecond of execution time compared to x86 equivalents, and in my testing, cold start times are consistently 5 to 15 percent faster.
Lambda x86_64 vs Lambda ARM64
Lambda x86_64
Lambda ARM64
For Java Lambda functions, the ARM64 cold start improvement is especially valuable since Java cold starts are notoriously long. The 400ms reduction at the p50 level translates to meaningfully better user experience for synchronous invocations.
One caveat: if your Lambda function uses native dependencies compiled for x86 (such as certain npm packages with C bindings or Python packages with compiled extensions), you need to ensure those dependencies have ARM64 variants. Most popular packages (sharp, bcrypt, numpy, pandas) ship ARM64 builds, but verify before deploying.
RDS Graviton Migration
Amazon RDS supports Graviton3 instance classes across PostgreSQL, MySQL, MariaDB, Oracle, and SQL Server. This is often the easiest migration win because it requires zero application changes.
RDS Instance Class Migration
resource "aws_db_instance" "primary" {
identifier = "production-primary"
engine = "postgres"
engine_version = "16.1"
# Graviton3 instance class
instance_class = "db.m7g.2xlarge"
# Storage configuration
allocated_storage = 500
max_allocated_storage = 2000
storage_type = "gp3"
storage_encrypted = true
# High availability
multi_az = true
# Maintenance window for architecture migration
apply_immediately = false
allow_major_version_upgrade = false
auto_minor_version_upgrade = true
# Performance Insights on Graviton3
performance_insights_enabled = true
performance_insights_retention_period = 731
tags = {
Architecture = "arm64-graviton3"
}
}
When you change an RDS instance class from db.m6i to db.m7g, AWS performs the migration during your next maintenance window (or immediately if you set apply_immediately = true). The instance reboots, starts on Graviton3 hardware, and your application reconnects. I have performed this migration on dozens of production databases and the downtime is typically under 60 seconds for Multi-AZ deployments.
db.m7g.2xlarge vs db.m6i.2xlarge monthly
RDS Graviton3 Savings
ElastiCache and MemoryDB
Redis and Memcached workloads on ElastiCache also support Graviton3 (cache.m7g, cache.r7g). For memory-intensive caching workloads, the DDR5 memory subsystem on Graviton3 delivers measurably lower latency at the p99 level, which matters for cache-dependent application architectures.
CI/CD Multi-Architecture Builds
The most common blocker for Graviton adoption is not the runtime environment but the build pipeline. If your Docker images are built for linux/amd64 only, they will not run on ARM instances. The solution is multi-architecture builds.
Docker Buildx for Multi-Arch Images
# Dockerfile - must use multi-arch base images FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY . . RUN npm run build FROM node:20-alpine AS runtime WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ EXPOSE 8080 USER node CMD ["node", "dist/server.js"]
The key requirement is that your base images must support both architectures. Official Docker Hub images for Node.js, Python, Go, Java, and most popular runtimes publish multi-arch manifests. Always use specific version tags rather than latest to ensure consistency.
GitHub Actions Multi-Arch Build
name: Build and Push Multi-Arch Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Login to ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push multi-arch image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ steps.ecr-login.outputs.registry }}/api:${{ github.sha }}
${{ steps.ecr-login.outputs.registry }}/api:latest
cache-from: type=gha
cache-to: type=gha,mode=max
This pipeline builds your Docker image for both linux/amd64 and linux/arm64, pushes a multi-arch manifest to ECR, and any Graviton3 or x86 instance pulling that image will automatically get the correct architecture variant.
Build time consideration: Cross-compilation via QEMU adds overhead. A build that takes 3 minutes on native architecture might take 8 to 12 minutes when cross-compiling. For faster builds, consider using AWS CodeBuild with ARM compute, or dedicated ARM64 GitHub Actions runners.
AWS CodeBuild with ARM
# buildspec.yml
version: 0.2
env:
variables:
ECR_REPO: '123456789.dkr.ecr.us-east-1.amazonaws.com/api'
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region us-east-1 | docker login --username
AWS --password-stdin $ECR_REPO
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)
- IMAGE_TAG=${COMMIT_HASH:=latest}
build:
commands:
- echo Building Docker image...
- docker build -t $ECR_REPO:$IMAGE_TAG .
- docker tag $ECR_REPO:$IMAGE_TAG $ECR_REPO:latest
post_build:
commands:
- echo Pushing Docker image...
- docker push $ECR_REPO:$IMAGE_TAG
- docker push $ECR_REPO:latest
resource "aws_codebuild_project" "arm_build" {
name = "api-arm64-build"
service_role = aws_iam_role.codebuild.arn
environment {
compute_type = "BUILD_GENERAL1_LARGE"
image = "aws/codebuild/amazonlinux2-aarch64-standard:3.0"
type = "ARM_CONTAINER"
privileged_mode = true
}
source {
type = "GITHUB"
location = "https://github.com/org/repo.git"
}
artifacts {
type = "NO_ARTIFACTS"
}
}
Using CodeBuild with ARM_CONTAINER type gives you native ARM64 builds without QEMU overhead. Build times match x86 builds, and the instances themselves cost less.
Monitoring Your Migration Impact
Migrating to Graviton3 without monitoring the impact is like optimizing a database query without benchmarking it. You need data to confirm that the migration is delivering the expected benefits and to catch any regressions early.
Key Metrics to Track
CloudWatch Dashboard for Migration Tracking
Create a dedicated CloudWatch dashboard that compares metrics across architecture dimensions. Tag all resources with an Architecture tag and use that dimension in your metric filters.
resource "aws_cloudwatch_dashboard" "graviton_migration" {
dashboard_name = "graviton3-migration-tracking"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
title = "CPU Utilization by Architecture"
metrics = [
["AWS/EC2", "CPUUtilization", "Architecture", "arm64",
{ stat = "Average", label = "Graviton3 (arm64)" }],
["AWS/EC2", "CPUUtilization", "Architecture", "x86_64",
{ stat = "Average", label = "Intel (x86_64)" }]
]
period = 300
region = "us-east-1"
view = "timeSeries"
}
},
{
type = "metric"
x = 12
y = 0
width = 12
height = 6
properties = {
title = "Request Latency by Architecture"
metrics = [
["Custom/App", "RequestLatency", "Architecture", "arm64",
{ stat = "p99", label = "Graviton3 p99" }],
["Custom/App", "RequestLatency", "Architecture", "x86_64",
{ stat = "p99", label = "Intel p99" }],
["Custom/App", "RequestLatency", "Architecture", "arm64",
{ stat = "p50", label = "Graviton3 p50" }],
["Custom/App", "RequestLatency", "Architecture", "x86_64",
{ stat = "p50", label = "Intel p50" }]
]
period = 300
region = "us-east-1"
view = "timeSeries"
}
}
]
})
}
Cost Tracking with AWS Cost Explorer Tags
Ensure your Architecture tag is activated as a cost allocation tag in AWS Billing. This lets you filter Cost Explorer by architecture and see exactly how much you are spending on Graviton3 versus x86.
resource "aws_ce_cost_allocation_tag" "architecture" {
tag_key = "Architecture"
status = "Active"
}
After activating the tag, give it 24 to 48 hours to populate in Cost Explorer. Then you can create reports showing cost trends segmented by processor architecture, making it trivial to demonstrate ROI to leadership.
Workload Compatibility Deep Dive
Let me share the specific compatibility findings from my migration work across various technology stacks.
Java and JVM Languages
Java is one of the best-supported ecosystems on Graviton3. Amazon Corretto, the AWS distribution of OpenJDK, includes ARM64-specific optimizations for garbage collection, JIT compilation, and intrinsics. Key requirements:
- JDK 11 or later is required for production-grade ARM64 support. JDK 17 is strongly recommended for the best performance.
- GraalVM Native Image works on ARM64 and produces excellent results for microservice startup time.
- Spring Boot works without modification. Spring Native (AOT compilation) also supports ARM64.
- Quarkus and Micronaut both have excellent ARM64 support with native compilation.
The one area to watch is JNI libraries. If your application uses native libraries through JNI (such as RocksDB, LevelDB, or custom C extensions), you need to ensure ARM64 variants are available. Most popular JNI libraries now ship ARM64 builds, but verify your specific dependencies.
Python Data Science Stack
The Python ecosystem is well-supported on ARM64, with a few nuances:
- NumPy, Pandas, scikit-learn, SciPy: All ship ARM64 wheels via pip. Performance is excellent thanks to ARM NEON optimizations in the underlying BLAS/LAPACK libraries.
- PyTorch: ARM64 builds are available. Inference performance on Graviton3 with bfloat16 support is competitive with CPU-only x86 inference.
- TensorFlow: ARM64 support is mature. The tensorflow-aarch64 package is available for Linux ARM64.
- ONNX Runtime: Native ARM64 builds deliver excellent inference performance, often 2x throughput versus x86.
The primary pain point is packages with C extensions that do not ship prebuilt ARM64 wheels. In these cases, pip will attempt to compile from source, which requires having build tools and development headers installed in your Docker image or environment.
Go and Rust
These compiled languages are arguably the easiest to migrate because the compiler handles everything. Go has had excellent ARM64 support since Go 1.16, and Rust has had tier-1 aarch64-unknown-linux-gnu support for years.
For Go, set GOARCH=arm64 in your build environment:
GOOS=linux GOARCH=arm64 go build -o app ./cmd/server
For Rust, add the ARM64 target and cross-compile:
rustup target add aarch64-unknown-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu
Both languages produce statically linked binaries by default (or can be configured to do so), which means you can build on any architecture and deploy the resulting binary to Graviton3 without worrying about shared library compatibility.
Node.js
Node.js works seamlessly on ARM64 from version 14 onward. The V8 JavaScript engine has had ARM64 JIT compilation support for years. The primary concern is npm packages with native addons:
- sharp (image processing): Ships ARM64 prebuilt binaries. Works out of the box.
- bcrypt: Ships ARM64 prebuilt binaries.
- better-sqlite3: Compiles from source on ARM64. Works correctly.
- grpc: ARM64 prebuilt binaries available.
If you encounter a package that fails on ARM64, check whether a newer version has added support. The npm ecosystem has overwhelmingly adopted ARM64 over the past two years.
Production Deployment Patterns
Pattern 1: Blue-Green Architecture Migration
For organizations that want a clean cutover rather than a gradual migration, blue-green deployment is the safest approach.
# Blue (current x86) environment
module "blue" {
source = "./modules/app-cluster"
architecture = "x86_64"
environment = "blue"
active = true
}
# Green (new Graviton3) environment
module "green" {
source = "./modules/app-cluster"
architecture = "arm64"
environment = "green"
active = false
}
# DNS-based traffic switching
resource "aws_route53_record" "app" {
zone_id = var.zone_id
name = "api.example.com"
type = "A"
alias {
name = var.active_env == "green" ? module.green.alb_dns : module.blue.alb_dns
zone_id = var.active_env == "green" ? module.green.alb_zone_id : module.blue.alb_zone_id
evaluate_target_health = true
}
}
Pattern 2: Spot Fleet with Graviton3 Priority
For cost-optimized workloads, Spot instances on Graviton3 deliver exceptional value. Graviton3 Spot instances typically have higher availability and lower interruption rates than x86 Spot because fewer customers are competing for ARM capacity.
| month | graviton3_spot | x86_spot | graviton3_ondemand | x86_ondemand |
|---|---|---|---|---|
| Jan | 62 | 71 | 100 | 100 |
| Feb | 58 | 68 | 100 | 100 |
| Mar | 55 | 65 | 100 | 100 |
| Apr | 60 | 70 | 100 | 100 |
| May | 57 | 67 | 100 | 100 |
| Jun | 54 | 64 | 100 | 100 |
The chart above shows relative cost indexed to on-demand pricing at 100. Graviton3 Spot instances typically run at 55 to 62 percent of x86 on-demand pricing, which means you are getting ARM performance benefits and Spot pricing benefits compounded together. For batch processing, CI/CD, and fault-tolerant workloads, this combination is hard to beat.
Pattern 3: Mixed-Architecture Auto Scaling
For production services that need high availability, configure your Auto Scaling group with both architecture types as a fallback strategy.
resource "aws_autoscaling_group" "mixed_arch" {
name = "app-mixed-arch"
desired_capacity = 6
max_size = 20
min_size = 3
vpc_zone_identifier = var.private_subnet_ids
mixed_instances_policy {
instances_distribution {
on_demand_base_capacity = 3
on_demand_percentage_above_base_capacity = 25
spot_allocation_strategy = "capacity-optimized"
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.graviton3.id
version = "$Latest"
}
override {
instance_type = "m7g.xlarge"
}
override {
instance_type = "m7g.2xlarge"
}
override {
instance_type = "c7g.xlarge"
}
override {
instance_type = "c7g.2xlarge"
}
# x86 fallback instances
override {
instance_type = "m6i.xlarge"
}
override {
instance_type = "m6i.2xlarge"
}
}
}
}
This configuration prioritizes Graviton3 instances but falls back to x86 if ARM capacity is unavailable. Combined with multi-arch Docker images, your application seamlessly runs on whichever architecture is available.
Common Migration Pitfalls and Solutions
After guiding dozens of migration projects, I have cataloged the most frequent issues teams encounter. Knowing these in advance saves significant debugging time.
Pitfall 1: Docker Images Built Only for amd64
Symptom: Tasks fail to start on ECS/EKS with exec format error.
Cause: The container image was built for linux/amd64 and scheduled on an ARM64 host.
Solution: Implement multi-arch builds as described in the CI/CD section above. Verify your images support both architectures by inspecting the manifest:
docker manifest inspect your-registry/image:tag
You should see entries for both amd64 and arm64 in the manifest list.
Pitfall 2: Native Dependencies Without ARM64 Support
Symptom: Build failures or runtime crashes from shared libraries.
Cause: A native dependency (npm addon, Python C extension, JNI library) does not have an ARM64 variant.
Solution: Check for updated versions of the dependency. If no ARM64 version exists, evaluate alternatives. In rare cases, you may need to compile from source within your Docker build, which requires adding the appropriate build tools to your Dockerfile.
Pitfall 3: Performance Regression in Specific Workloads
Symptom: Overall throughput is lower on Graviton3 than expected.
Cause: Usually traced to JVM version (pre-JDK 11), unoptimized memory allocation patterns, or workloads that rely heavily on x86-specific SIMD instructions (AVX-512).
Solution: Ensure you are running the latest stable runtime version. For Java, move to JDK 17 with Amazon Corretto. For workloads dependent on AVX-512, Graviton3's SVE provides equivalent vectorization but code may need recompilation with appropriate flags.
Pitfall 4: Terraform State Drift During Migration
Symptom: Terraform plans show unexpected changes after switching instance types.
Cause: Changing instance families (e.g., m6i to m7g) may trigger AMI changes and launch template updates that cascade through your Terraform state.
Solution: Use the modular architecture pattern shown earlier, where the architecture is a variable that cleanly maps to instance families, AMI filters, and launch templates. Plan carefully and review the full diff before applying.
Pitfall 5: Monitoring Agent Compatibility
Symptom: Observability gaps after migration. Missing metrics or traces from Graviton3 instances.
Cause: Some monitoring agents (Datadog, New Relic, Dynatrace) required specific ARM64 builds that were not available in older versions.
Solution: As of 2025, all major observability platforms ship ARM64 agents. Ensure you are running the latest version of your monitoring agent and verify ARM64 support in their documentation before migrating.
Total Cost of Ownership Analysis
Let me walk through a realistic TCO analysis for a mid-size production environment migrating from x86 to Graviton3.
Scenario: 50 EC2 instances (mix of c6i.2xlarge and m6i.2xlarge), 5 RDS db.m6i.xlarge instances, 200 Lambda functions, 10 ECS services on Fargate.
| category | x86_annual | graviton3_annual |
|---|---|---|
| EC2 Instances | 438000 | 342000 |
| RDS Databases | 65700 | 51200 |
| Lambda Functions | 24000 | 19200 |
| Fargate Tasks | 87600 | 70000 |
| Total | 615300 | 482400 |
50 EC2 + 5 RDS + 200 Lambda + 10 ECS services
Annual TCO Savings
That is nearly 133,000 dollars per year for a modestly sized environment. For enterprises running hundreds or thousands of instances, the savings scale into the millions. And this calculation does not account for the performance improvements that often allow further right-sizing, which can add another 10 to 15 percent on top.
The migration itself requires engineering effort, which is a real cost. In my experience, a team of two to three engineers can migrate a 50-instance environment in four to six weeks, including testing and staged rollout. At fully loaded engineering costs, that is roughly 40,000 to 60,000 dollars of effort. The migration pays for itself within six months, and then it is pure savings from that point forward.
When NOT to Use Graviton3
Despite my enthusiasm for Graviton3, there are legitimate cases where x86 remains the better choice.
Windows workloads. Graviton instances run Linux only. If you are running Windows Server on EC2, Graviton is not an option.
AVX-512 dependent workloads. Some scientific computing, financial modeling, and media encoding workloads are heavily optimized for Intel's AVX-512 vector instructions. While Graviton3's SVE provides similar capabilities, migrating hand-tuned AVX-512 code requires significant effort.
Legacy binary dependencies. If your application depends on commercial software distributed as x86-only binaries and the vendor has no ARM64 roadmap, you are stuck on x86 until the vendor situation changes.
Extremely latency-sensitive workloads. In my benchmarks, Graviton3 matches x86 at p50 and p95 but occasionally shows slightly higher p99.9 latency for certain workloads. If you are in ultra-low-latency territory (sub-millisecond requirements), benchmark carefully.
Looking Ahead: Graviton4 and Beyond
AWS launched Graviton4 in late 2024 with 96 Neoverse V2 cores, delivering 30 percent better compute performance, 50 percent more cores, and 75 percent higher memory bandwidth than Graviton3. The R8g instances based on Graviton4 are already available, with C8g and M8g rolling out throughout 2025.
The trajectory is clear. AWS is investing billions in custom silicon, and each generation delivers meaningful improvements in price-performance. Organizations that migrate to Graviton3 today position themselves to seamlessly adopt Graviton4 when the instance families are broadly available, because the migration from one Graviton generation to the next is simply an instance type change, no application modifications required.
| generation | performance_index | price_performance |
|---|---|---|
| Graviton1 | 100 | 100 |
| Graviton2 | 200 | 240 |
| Graviton3 | 250 | 320 |
| Graviton3E | 275 | 340 |
| Graviton4 | 325 | 420 |
The price-performance trajectory is accelerating faster than raw performance because each generation not only gets faster but also gets more power-efficient, and AWS passes those efficiency gains through as lower pricing.
Migration Prioritization Framework
If you are convinced but unsure where to start, here is the prioritization framework I recommend based on effort-to-impact ratio.
Start here (high impact, low effort):
- Lambda functions: change one line, deploy, done
- RDS instances: change the instance class during a maintenance window
- Stateless web servers and API services on EC2 or Fargate
Second wave (high impact, moderate effort):
- ECS and EKS container workloads (requires multi-arch builds)
- Batch processing and data pipeline workers
- ElastiCache and MemoryDB instances
Third wave (moderate impact, higher effort):
- Java services requiring JDK upgrades
- Workloads with native dependency compatibility issues
- Services with complex CI/CD pipelines requiring multi-arch build setup
Defer or skip:
- Windows workloads
- Workloads with hard x86 binary dependencies
- Systems in active feature development where migration adds risk
Conclusion
AWS Graviton3 represents a genuine paradigm shift in cloud cost optimization. The 20 to 40 percent savings are real, the performance is competitive or better across nearly every workload type, and the migration path is well-documented and well-supported. The ARM software ecosystem has reached a maturity level where compatibility concerns are the exception rather than the rule.
The organizations that are getting the most value from Graviton3 are the ones that treat it not as a one-time migration project but as an architectural standard. They default to Graviton for new workloads, maintain multi-arch CI/CD pipelines as a matter of course, and continuously migrate existing workloads as part of their infrastructure optimization practice.
If you take one thing from this guide, let it be this: start with Lambda and RDS. Those two services deliver immediate savings with near-zero risk and near-zero effort. Once you see the cost reductions in your AWS bill, the business case for broader migration will make itself.
The compute economics of cloud are shifting permanently toward ARM. Graviton3 is not a trend to watch. It is an optimization to implement, starting today.
