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.xlarget3.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:

PillarRisk if ignored
MemoryOOM kills after downsize — caches and runtimes need RAM
Network throughputSmaller types cap bandwidth — hurts API and DB-heavy apps
EBS IOPS / burstDatabases 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:

  1. Staging first — Apply the smaller type in staging; run integration tests under load.
  2. Rollback ready — Note original instance type; keep Terraform vars on a branch for a 60-second revert.
  3. Low-traffic window — Execute in prod during your quietest period.
  4. 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 costBaseline~20% lower
ThroughputBaselineOften 15–30% better
CoresLogical (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

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:

  1. 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'
  2. 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
    }
  3. Deploy and soak-test 1 week — watch for random crashes, memory leaks, or CPU spikes that don’t happen on x86.

  4. If stable, roll out to prod gradually (10% → 50% → 100%).

  5. 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/c5 with current gen (m7i, c7g Graviton) 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.