Almost every AWS account has one: an EC2 instance somebody launched for a demo, a migration, or a "temporary" test box, and then forgot. It has been running for months. Nobody has logged into it. It does nothing but sit there accruing an hourly charge. You suspect it exists — you just can't easily prove which instances are the dead weight. This post shows you how to find genuinely idle instances, what they really cost you, and how to decide between stopping, terminating, and downsizing.
What does "idle" actually mean?
Here's the trap most cleanup advice falls into: it equates low CPU with idle. CPU utilization alone is a bad signal in both directions.
A box sitting at 2% CPU while serving a steady trickle of network traffic — a low-volume internal API, a message consumer, a health-checked service — is not idle. Terminate it and something breaks. Meanwhile a box at 15% CPU that does nothing but run a cron job once an hour is effectively idle for a compute you're paying for around the clock.
So the honest definition is behavioural, not a single number: an instance is idle when CPU is low and network traffic is negligible over a sustained window. You need CPUUtilization alongside NetworkIn and NetworkOut, looked at together.
And the cost of getting it wrong — of leaving an idle box running because you never looked — is not abstract. A single t3.large on-demand runs about $60/month; an m5.xlarge closer to $140/month. One forgotten instance is a rounding error. A dozen, spread across three regions and two accounts, is a five-figure annual bill for compute that does nothing. The reason it survives is that no one line item looks alarming — the waste is in the aggregate, which is precisely why it hides.
CloudWatch gives you CPU, network, and disk I/O out of the box, but not memory — RAM utilization requires the CloudWatch agent installed on the instance. This is why CPU-only "rightsizing" advice is unreliable: an instance can look idle on CPU while its workload is actually memory-bound. Treat CPU as necessary but not sufficient, and confirm with network activity before you act.
How do I find them with CloudWatch?
Start by pulling CPU and network metrics for a candidate instance over a 14-day window. Fourteen days matters: a 24-hour snapshot will flag a box that runs a heavy weekly batch job every Sunday as idle on a Tuesday. Two weeks captures weekly cycles.
# Average CPU over the last 14 days, one datapoint per day
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
--start-time "$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 86400 \
--statistics Average Maximum \
--region us-east-1 \
--output table
Then do the same for NetworkIn and NetworkOut (same namespace, --statistics Sum). An instance whose 14-day CPU Maximum never crosses ~10% and whose network bytes are near zero is a strong idle candidate. If CPU is low but network Sum is substantial, it's serving traffic — leave it alone and look elsewhere.
If you'd rather not script this, AWS Compute Optimizer does a version of the same analysis for you and will label instances as "over-provisioned" with a recommended size. Treat its output as a starting shortlist rather than gospel — it leans on CPU and network too, so it inherits the same memory blind spot — but it's a fast way to see which instances are worth investigating before you pull metrics by hand.
To list every running instance first, so you know what to check:
aws ec2 describe-instances \
--filters Name=instance-state-name,Values=running \
--query 'Reservations[].Instances[].[InstanceId,InstanceType,LaunchTime,Tags[?Key==`Name`]|[0].Value]' \
--output table
Should I stop, terminate, or downsize?
Once you've confirmed an instance is idle, the fix depends on why it's there. These are three different actions with three different billing consequences.
| Action | Compute charge | EBS / storage charge | When to use it |
|---|---|---|---|
| Stop | Halts | Still billed | Dev/test box you'll want back; non-production off-hours |
| Terminate | Halts | Root volume released | Truly abandoned; nothing depends on it |
| Downsize | Reduced | Unchanged | Instance is used but too big (see below — that's not idle) |
Stopping an instance halts the compute charge, but it does not stop the charges for its EBS volumes, any EBS snapshots of them, or an Elastic IP that was attached. A stopped instance with a 200 GB gp3 root volume still costs you roughly $16/month for storage alone, indefinitely. This is the single most common EC2 billing surprise. If you're stopping an instance to save money and you want the storage cost gone too, you have to deal with the volume separately — see how to find and delete orphaned EBS volumes and snapshots.
If the instance is genuinely abandoned and nothing depends on its data, terminate it — that releases the root volume and stops the storage bleed. If it's a dev box you'll want on Monday, stop it (and accept the storage cost, or snapshot-and-terminate). The safe middle path when you're fairly sure but not certain: take an AMI or a snapshot of the volumes first, then terminate. A snapshot is a few cents per gigabyte per month to keep around; a full running instance plus its available volumes is far more. You can always relaunch from the image if it turns out someone needed it. Reach for the AWS Console (EC2 → Instances → Instance state) or:
# Reversible: stops compute, keeps the instance and its volumes
aws ec2 stop-instances --instance-ids i-0123456789abcdef0
# Irreversible: releases the instance and its root volume
aws ec2 terminate-instances --instance-ids i-0123456789abcdef0
Isn't a consistently busy-but-small instance a different problem?
Yes — and conflating the two is a classic mistake. An instance sitting at a steady 20% CPU is not idle. It's doing real work; it's just the wrong size for that work. The fix isn't to stop it, it's to move it to a smaller instance type (or a different family) so you stop paying for headroom you never use.
That's a separate finding — oversized, not idle — with a separate remedy: change the instance type during a maintenance window (a stop/start, so plan for brief downtime) rather than shutting the workload off. If the metrics say "used, but over-provisioned," see Oversized EC2 instance for how we flag it and estimate the smaller size. Getting this distinction right is the difference between saving money and causing an incident.
How do I stop them coming back?
A one-off cleanup feels great and then silently refills over the next quarter. Two habits keep it from regrowing:
- Schedule non-production instances. Dev, staging, and CI boxes rarely need to run nights and weekends. An instance scheduler (AWS Instance Scheduler, or a simple Lambda on an EventBridge cron) that stops them at 7pm and starts them at 8am cuts their runtime — and cost — by more than half with zero effort after setup.
- Tag for ownership. The reason idle instances accumulate is that nobody knows who owns them, so nobody dares delete them. A required
ownerandpurposetag at launch means the next person cleaning up can ask a name instead of guessing. Enforce it with an SCP or a tag policy if you can.
One thing that will not save you here: a Savings Plan or Reserved Instance. Those discount the rate you pay for compute you run — they do nothing about compute you shouldn't be running at all. Committing to a three-year Savings Plan that covers a fleet half-full of idle instances just locks in the waste at a discount. Right-size and clean up first, then buy commitments against the footprint you actually need. Doing it in the other order is a surprisingly common and expensive mistake.
How do I find every idle instance across my account?
Checking one instance by hand is easy. Doing it for every instance in every region — pulling 14 days of CPU and network, cross-referencing tags, and separating truly idle from merely oversized — is the tedious part, and it's exactly where the waste hides. That's what a scan is for. Parsivex checks each region, correlates CPU with network activity over a real window, separates idle instances from over-provisioned ones, and estimates the monthly savings for each.
For what these findings mean once they show up in your report, see Idle EC2 instance and Oversized EC2 instance.