Idle instances: the basics
What does it mean for an EC2 instance to be idle?
An idle EC2 instance is one that's powered on and accruing hourly charges but doing no useful work. CPU sits below a few percent, network traffic is essentially flat, no users or services are connected, and the workload it was originally provisioned to run is either finished, abandoned, or has quietly been moved elsewhere. The instance is on, the bill is real, and nothing on it is earning that money back.
Idle isn't the same as oversized. An oversized instance has a workload running on it, the box is just too big for what it does. An idle instance has no meaningful workload at all. The fix is different too: oversizing wants a smaller instance, idleness wants the instance gone (stopped, terminated, or scheduled off).
Idle instances are flagged when 14 days of telemetry show consistently low CPU (peak utilisation below 5%) and trivial network throughput in either direction (less than 5MB/day). AWS Compute Optimizer publishes an Idle finding type specifically for this signal, distinct from its over-provisioned recommendation track.
In this lesson you'll learn what "idle" actually means in CloudWatch terms, why the basic CPUUtilization metric alone isn't enough to make the call, how to investigate an idle candidate before you delete it, and how to choose between stopping and terminating. You'll see real AWS CLI output for the detection pipeline, and finish with a prevention plan that keeps idle boxes from accumulating in the first place.
The forgotten POC tax
In a recent FinOps Foundation survey, idle compute accounted for roughly 30% of all cloud waste at organisations without a tagging policy, and most of it was traced back to short-lived proof-of-concept work that simply never got cleaned up. The pattern is depressingly consistent: a developer spins up a t3.xlarge for a Friday demo, the demo goes well, the demo gets forgotten, and the instance runs for fourteen months at $120 a month until someone notices. Multiply that across an engineering org of a few hundred people and the "experiment budget" becomes a permanent line item.
Reclaiming idle instances in action
Nina runs platform engineering at a mid-size SaaS. A FinOps review surfaces 47 EC2 instances flagged HIGH severity for idleness, projected monthly savings between $50 and $400 each, total around $9k/month.
She pulls the first candidate: i-0d4e5f6a7b8c9d0e1, an m5.xlarge in us-east-1 tagged Environment=dev, Owner= (empty), running for 312 days. CloudWatch shows 0.8% average CPU and a NetworkOut floor of 14 KB/s, pure heartbeat traffic from the SSM agent.
Before she terminates anything she runs the investigation checklist: who launched it, when did anyone last log in, is there a dependency she can't see from the metrics, and is the EBS volume holding something she'd regret losing. Five minutes of due diligence saves an outage later.
Start with Compute Optimizer's authoritative Idle finding: it correlates CPU and network utilisation for you. The Idle finding has its own API, separate from the right-sizing track.
Compute Optimizer's Idle finding for the m5.xlarge candidate.
Before you delete anything, check when a human last touched the box. SSM keeps a session history that's far more reliable than guessing from CloudTrail.
SSM tells you the agent is alive but nothing meaningful has happened on the host in 10 months.
Idle detection under the hooddeep dive
CloudWatch's default CPUUtilization metric is necessary but not sufficient. An instance can sit at 1% CPU while still doing real work, holding open database connections, acting as a bastion, serving a low-traffic internal endpoint, or running a scheduled job that fires once a day. CPU alone will flag all of these as idle and you'll generate outages by acting on the signal naively.
Compute Optimizer's Idle finding for an EC2 instance combines two signals: peak CPU utilisation below 5% and network I/O of less than 5MB/day across the 14-day lookback window (for G and P GPU instance types it also factors in GPU utilisation, encoder usage, and GPU memory). Both have to be quiet: that's what separates idle from "low traffic but real." The 14-day lookback catches weekly cron jobs. (EBS read/write IOPS are how Compute Optimizer judges idle EBS volumes, and ELB target health is a separate manual check you can run yourself, neither is part of the EC2-instance idle definition.)
Billing-wise, a running instance accrues two costs: the per-second EC2 rate for the instance type, and the GB-month rate for every attached EBS volume. Stopping the instance halts the EC2 charge immediately at the next second boundary but EBS keeps billing. Terminating releases the EBS volumes (unless DeleteOnTermination=false) and any attached Elastic IPs only stop billing once they're explicitly released, a stopped instance with an associated EIP still costs ~$3.60/month for the address alone.
# Pull 14 days of CPU + network maxima in one go to validate Compute Optimizer's call.
aws cloudwatch get-metric-data \
--start-time $(date -u -d '14 days ago' +%FT%TZ) \
--end-time $(date -u +%FT%TZ) \
--metric-data-queries '[
{"Id":"cpu","MetricStat":{"Metric":{"Namespace":"AWS/EC2","MetricName":"CPUUtilization","Dimensions":[{"Name":"InstanceId","Value":"i-0d4e5f6a7b8c9d0e1"}]},"Period":3600,"Stat":"Maximum"}},
{"Id":"netin","MetricStat":{"Metric":{"Namespace":"AWS/EC2","MetricName":"NetworkIn","Dimensions":[{"Name":"InstanceId","Value":"i-0d4e5f6a7b8c9d0e1"}]},"Period":3600,"Stat":"Maximum"}},
{"Id":"netout","MetricStat":{"Metric":{"Namespace":"AWS/EC2","MetricName":"NetworkOut","Dimensions":[{"Name":"InstanceId","Value":"i-0d4e5f6a7b8c9d0e1"}]},"Period":3600,"Stat":"Maximum"}}
]'
# Also check ELB target health if the instance is behind a load balancer.
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web/abc123 \
--targets Id=i-0d4e5f6a7b8c9d0e1 What is the impact of leaving idle instances running?
The direct cost is the easy one: an idle m5.xlarge in us-east-1 is roughly $140/month for the EC2 charge alone, plus another $8-15/month for the typical 100 GB gp3 root volume. Multiply across a few dozen forgotten boxes and you're looking at $5-10k a month of pure waste. The Action Hub typically surfaces these in the $50-$400/month range per instance, with the high end being r5/m5 multi-xlarge dev boxes left running over a holiday shutdown.
The hidden cost is worse. Idle instances pollute every cost report: they inflate per-service spend, distort tag-based showback, and make it impossible to see real workload trends because the baseline is contaminated with noise. A 20% MoM increase on "EC2 / dev / team-payments" might be real growth, or it might be three idle boxes someone spun up; you can't tell without going line-by-line.
From a security perspective, idle instances are also stale instances. They're not getting patched, their IAM role permissions are still active, the SSH/RDP attack surface is still exposed, and any credentials baked into user data are still recoverable. Compliance frameworks (CIS, NIST, PCI) flag unpatched and orphan compute as a control failure, an idle box is one CVE away from being an unmonitored foothold.
Finally, idle instances corrupt the Reserved Instance and Savings Plan math. Your RI coverage report will show "high utilisation" because the instance is technically running 24/7, even though nothing useful is happening on it. You'll renew commitments based on phantom demand and then be stuck paying for capacity you never actually needed.
How do you reclaim idle instances safely?
Reclaiming idle compute is a four-step loop. Skip step one and you'll delete something that was load-bearing in a way the metrics didn't show.
1. Investigate before you delete
For every candidate, pull the instance tags (owner, environment, cost-centre), check SSM for last-association and last-login timestamps, scan CloudTrail for any API calls targeting the instance in the last 30 days, and look for ENIs or security-group references from other resources. If the owner tag is empty or the owner has left, post in #cloud-cleanup with a 7-day deadline before acting. A stopped instance you can restart is far cheaper than an outage.
2. Stop first, terminate later
Stopping halts the EC2 charge immediately while preserving the EBS volume, the instance ID, and any private IP. It's the reversible move. Tag the stopped instance with Status=pending-termination and a date 14 days out; if nobody complains in that window, terminate. Watch out for the gotchas: an attached Elastic IP keeps billing while stopped (release it), instance-store volumes are gone the moment you stop, and DeleteOnTermination=false EBS volumes will linger after termination unless you delete them explicitly.
3. Snapshot before terminating anything stateful
If the EBS volume holds anything you can't reproduce from source (application data, manual config, half-finished experiments), take an EBS snapshot and an AMI before you terminate. Snapshots are cheap (~$0.05/GB-month) and reversible; a terminated instance with DeleteOnTermination=true is not. Tag the snapshot with the original instance ID and a retention window so it doesn't become its own waste problem six months later.
4. Prevent recurrence with policy, not vigilance
Enforce a tagging policy at the org level: every EC2 instance must have Owner, Environment, and Expires tags or the launch fails (SCP + AWS Config rule). Schedule non-prod instances to stop nightly and on weekends via EventBridge + Instance Scheduler: most dev fleets are idle 130+ hours per week. Run Compute Optimizer's Idle finding as a weekly digest into the FinOps channel. Idle instances should never get to 312 days without anyone noticing again.
# Step 1: stop the instance (reversible, halts compute billing immediately).
aws ec2 stop-instances --instance-ids i-0d4e5f6a7b8c9d0e1
# Step 2: tag with a termination deadline.
aws ec2 create-tags --resources i-0d4e5f6a7b8c9d0e1 \
--tags Key=Status,Value=pending-termination Key=TerminateAfter,Value=2026-05-29
# Step 3: snapshot the root volume before final termination.
aws ec2 create-snapshot --volume-id vol-0123456789abcdef0 \
--description 'pre-termination snapshot of i-0d4e5f6a7b8c9d0e1' \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=SourceInstance,Value=i-0d4e5f6a7b8c9d0e1}]'
# Step 4: terminate after the deadline passes.
aws ec2 terminate-instances --instance-ids i-0d4e5f6a7b8c9d0e1 Quick quiz
Question 1 of 5An EC2 instance is flagged HIGH for idleness: 1.2% maximum CPU, 14 KB/s NetworkOut, no ELB target group attached, last SSM association ran 10 months ago. What's the right next move?
You scored
0 / 5
Keep learning
Go deeper on idle-instance detection, lifecycle automation, and the prevention layer.
- AWS Compute Optimizer: Idle resources How the Idle finding is calculated and how to read its utilisation metrics.
- AWS Instance Scheduler Solution accelerator for scheduled stop/start of non-prod EC2 and RDS, the standard fix for weekend waste.
- AWS Systems Manager Session Manager Audit who logged into which instance and when, the source of truth for last-touch investigation.
- FinOps Foundation: Usage Optimization Where idle reclamation sits in the FinOps lifecycle alongside right-sizing and commitment planning.
You've completed Reclaim idle EC2 instances. You now know how to read the idle signal (peak CPU below 5% plus network I/O under 5MB/day over the 14-day lookback), why Compute Optimizer's Idle finding is the authoritative call, and how to run the investigate-stop-snapshot-terminate loop without breaking anything load-bearing. Wire up tagging policy and a scheduled stop/start and the 312-day forgotten POC stops being a recurring line on your bill.
Back to the library