How to Right-Size EC2 Instances Without a FinOps Team (2026)
Published · Updated
Most rightsizing guides assume a FinOps team, a cost review board, and weeks to analyze dashboards. Lean teams and solo operators need a fast, data-driven process you can run between feature work — not another enterprise program.
Where to get utilization data for free
AWS Compute Optimizer
It already analyzes your fleet using CloudWatch history (14 days default; up to 3 months with enhanced metrics).
- Flags instances as Optimized, Under-provisioned, or Over-provisioned
- Suggests exact target types (e.g.
m6i.xlarge→t3.large) with estimated monthly savings
Open it before buying third-party cost SaaS.
CloudWatch metrics
For manual validation, pull 14 days of metrics — include weekdays and weekends so you see real traffic baselines.
Install the CloudWatch Agent if you only have default CPU metrics. Rightsizing without memory data is guesswork.
Avoid the “low CPU = downsize” trap
An instance at 5% CPU is not automatically waste. Check four pillars before changing size:
| Pillar | Risk if ignored |
|---|---|
| Memory | OOM kills after downsize — caches and runtimes need RAM |
| Network throughput | Smaller types cap bandwidth — hurts API and DB-heavy apps |
| EBS IOPS / burst | Databases choke on downsized burst limits |
| CPU credits (t3/t4g) | Baseline traffic can drain credits after a “successful” downsize |
Rule: Never downsize on CPU alone. Confirm memory, network, and disk with the agent and 14-day graphs.
Safe downsizing process
Treat a size change like any infra deploy:
- Staging first — Apply the smaller type in staging; run integration tests under load.
- Rollback ready — Note original instance type; keep Terraform vars on a branch for a 60-second revert.
- Low-traffic window — Execute in prod during your quietest period.
- Monitor 48 hours — Watch CPU credit balance, memory, and p95 latency.
Swap families without shrinking
When you cannot drop a size tier, move to a more efficient family:
Graviton (x86 → ARM)
For Node, Python, Java, Go, or .NET Core workloads, migrate from c6i/m6i to c8g/m8g Graviton:
x86 (c7i.2xlarge) | Graviton (c8g.2xlarge) | |
|---|---|---|
| On-demand cost | Baseline | ~20% lower |
| Throughput | Baseline | Often 15–30% better |
| Cores | Logical (HT) | Physical |
Same memory footprint, lower bill, often better performance.
Automation for lean teams
Dev/staging: stop instances on a schedule
Non-prod waste is often idle hours, not oversized types. Use Instance Scheduler or a Lambda on tags like Environment=dev:
- Stop at 19:00, start at 08:00
- 50–65% savings on those instances without touching prod sizing
Paid tooling
Consider nOps, ProsperOps, or similar only when spend exceeds ~$10k/month and manual RI/rightsizing work exceeds license cost.
Hands-on: Compute Optimizer analysis script
Pull over-provisioned instances directly from the AWS CLI instead of clicking through the console:
#!/bin/bash
# get-overprovisioned.sh — list instances Compute Optimizer flags as over-provisioned
aws compute-optimizer get-ec2-instance-recommendations \
--region ap-south-1 \
--query 'instanceRecommendations[?finding==`Overprovisioned`].[
instanceArn,
currentInstanceType,
recommendationOptions[0].instanceType,
recommendationOptions[0].savingsOpportunity.estimatedMonthlySavings.value
]' \
--output table
echo "---"
echo "Next steps:"
echo "1. Pick the top 3 by estimated savings"
echo "2. Check CloudWatch memory & network (not just CPU)"
echo "3. Downsize in staging first with 48-hour monitoring"
Sample output:
Saving Instance Current Type Recommended Monthly Savings (USD)
─────────────────── ──────────────── ─────────────── ────────────────── ────────────
prod-api-3 m5.2xlarge t3.xlarge $180
prod-cache-1 c5.large t3.medium $45
dev-runner-2 r5.xlarge m5.large $120
In Terraform (safe rollback):
variable "api_instance_type" {
default = "m5.2xlarge" # Keep old value here
# description = "Override to t3.xlarge after staging validation"
}
resource "aws_instance" "prod_api" {
instance_type = var.api_instance_type
# ... rest of config
}
When ready to downsize:
# Staging first
terraform apply -var="api_instance_type=t3.xlarge" -var-file=staging.tfvars
# If monitoring shows OK after 48 hours, apply to prod
terraform apply -var="api_instance_type=t3.xlarge" -var-file=prod.tfvars
Memory profiling: When CloudWatch isn’t enough
CloudWatch Agent captures basic memory, but some runtimes (Go, Java) show low RAM because they’re using the kernel page cache or JIT compilation buffer. For deeper insight:
Node.js / Python:
# SSH into instance, check resident memory
ps aux | grep node
# VIRT = virtual, RSS = resident (actual RAM used)
Java:
jps -lm # list running JVMs
jstat -gc <pid> 500 # garbage collection stats every 500ms
# Look for Full GC frequency — if spiking after downsize, you hit the heap ceiling
Go:
# Check runtime stats
curl http://localhost:6060/debug/pprof/heap
# If heap size is 70%+ of instance memory, you're memory-constrained
The burstable trap: t3/t4g instance credits
t3 and t4g instances are fundamentally different. They come with a “burst” model where:
- Baseline CPU = baseline performance (e.g., t3.large = 20% of a full core)
- CPU Credits = tokens accumulated at baseline; spent when you exceed baseline
- Once credits = 0 → CPU throttled to baseline, latency spikes
Scenario: Your app idles at 5% CPU (builds credits)
Downsize: m5.large (2 CPU) → t3.large (20% baseline per core)
Week later: Deploy heavy job → App spikes to 50% CPU
Result: Credits deplete → throttled to 20% → request timeouts
Check before downsize:
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUCreditBalance \
--dimensions Name=InstanceId,Value=i-1234567890abcdef0 \
--start-time 2026-07-01T00:00:00Z \
--end-time 2026-07-08T00:00:00Z \
--period 3600 \
--statistics Average
If credits regularly hit zero, you’re already maxed out — upsizing t3 tier (t3.xlarge) or switching to t4g Graviton is safer than downsize.
ARM migration: x86 to Graviton step-by-step
Graviton (c8g, m8g) is 20% cheaper on-demand and often faster, but requires testing:
-
Check app compatibility (works on Node, Python, Java, Go, Rust, .NET Core):
# Query AMI — is it ARM-compatible? aws ec2 describe-images \ --image-ids ami-0c55b159cbfafe1f0 \ --query 'Images[0].Architecture' -
Spin up staging t4g instance (Graviton):
resource "aws_instance" "staging_test" { ami = "ami-0c55b159cbfafe1f0" # ARM AMI instance_type = "t4g.large" # Graviton # ... same config as prod } -
Deploy and soak-test 1 week — watch for random crashes, memory leaks, or CPU spikes that don’t happen on x86.
-
If stable, roll out to prod gradually (10% → 50% → 100%).
-
Known gotchas:
- Docker images must be multi-arch or ARM-specific
- Some C extensions in Python packages may not have ARM binaries
- Observability agents (Datadog, New Relic) sometimes lag on Graviton support
Quarterly 60-minute checklist
First Tuesday each quarter:
- Export top 10 over-provisioned instances from Compute Optimizer (use script above)
- Validate memory + network for the top 3 with CloudWatch Agent
- Replace legacy
m5/c5with current gen (m7i,c7gGraviton) in staging - Terminate sandbox instances with no meaningful traffic for 30+ days (snapshot EBS first)
- Check t3/t4g CPU credit balance — upsizing tier if regularly depleting
Time saved: 50–65% on non-prod spend via Instance Scheduler = ₹15,000–30,000/month for typical 5-instance dev environment.
Rightsizing is a habit, not a one-time project. Pair this with solo FinOps guardrails and AWS cost-cutting playbook.
Browse cloud and DevOps roles when you are ready to apply.