Six places where an AWS bill quietly leaks money, how to find each one in the console or the CLI, and what to do about it. No tools required, just an afternoon's discipline.
Most AWS bills carry a few percent of pure waste: resources that nobody uses but everybody pays for. They are easy to find once you know where to look, and each one has a two-minute fix. This guide walks through the six that we find in almost every account we review, in the order that pays back fastest. Each step shows the console path and the equivalent CLI command, so you can do this with the access you already have.
You need read access to EC2, VPC, CloudWatch and Budgets, and a terminal with the AWS CLI signed in. Everything here is read-only; the fixes are up to you.
1. Unattached EBS volumes
When an instance is terminated, its root volume usually goes with it, but extra volumes stay behind unless "delete on termination" was set. They keep billing by the gigabyte-month, forever, attached to nothing.
Console: EC2 › Volumes, filter by Volume state = Available. Sort by size.
CLI:
aws ec2 describe-volumes --filters Name=status,Values=available \
--query 'Volumes[].[VolumeId,Size,VolumeType,CreateTime]' --output table
Run it in every region you use; volumes are regional. A volume that has been available for more than a month belongs to nothing that is coming back.
What to do: snapshot the ones you are unsure about (a snapshot costs a fraction of the volume), then delete the volumes. Check the tags first: a Name tag such as prod-db-data deserves a question before a deletion.
2. Old snapshots
Snapshots accumulate because nothing deletes them: AMIs you built two years ago, backups from a database that was migrated, copies made "just in case" before a change. Each one bills by the gigabyte-month of stored data.
Console: EC2 › Snapshots, Owned by me, sort by Started.
CLI (snapshots older than six months):
aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[?StartTime<=`2026-04-20`].[SnapshotId,VolumeSize,StartTime,Description]' \
--output table
What to do: keep the ones referenced by an AMI you still launch (aws ec2 describe-images --owners self lists them), keep anything a retention policy requires, and delete the rest. Then stop the problem from coming back: Data Lifecycle Manager can create and expire snapshots on a schedule, so nothing depends on a person remembering.
3. Idle NAT gateways
A NAT gateway costs by the hour whether or not any traffic goes through it, plus a per-gigabyte charge for the traffic that does. Teams create one per VPC, or one per availability zone, and forget them when the workload moves.
Console: VPC › NAT gateways. Then CloudWatch › Metrics › NAT Gateway, metric BytesOutToDestination, two weeks, statistic Sum.
CLI:
aws ec2 describe-nat-gateways --filter Name=state,Values=available \
--query 'NatGateways[].[NatGatewayId,VpcId,SubnetId]' --output table
aws cloudwatch get-metric-statistics --namespace AWS/NATGateway \
--metric-name BytesOutToDestination --dimensions Name=NatGatewayId,Value=nat-0123456789abcdef0 \
--start-time 2026-10-06T00:00:00Z --end-time 2026-10-20T00:00:00Z --period 86400 --statistics Sum
A gateway with no datapoints, or a few kilobytes a day, is paying for nothing.
What to do: delete it, and delete the route table entry that pointed at it. If the private subnets still need to reach AWS services, a gateway endpoint for S3 or DynamoDB is free, and an interface endpoint is often cheaper than a NAT gateway for a single service.
4. Unused Elastic IPs
Since February 2024, every public IPv4 address bills by the hour, attached or not. An Elastic IP that was released from a terminated instance, or allocated for a project that never shipped, costs the same as one serving traffic.
Console: VPC › Elastic IPs; an empty Associated instance ID column is the sign.
CLI:
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output table
What to do: release the ones nobody is waiting for. If an address is reserved on purpose (it is in a partner's allow list), tag it with the reason so the next reviewer does not have to ask.
5. Stopped instances that still pay for storage
Stopping an instance stops the compute charge, not the storage. Its EBS volumes bill every month, and so does any Elastic IP that stays attached. Instances stopped "for a while" in a migration tend to stay stopped for a year.
Console: EC2 › Instances, filter by Instance state = stopped; the State transition reason column tells you when.
CLI:
aws ec2 describe-instances --filters Name=instance-state-name,Values=stopped \
--query 'Reservations[].Instances[].[InstanceId,InstanceType,StateTransitionReason,BlockDeviceMappings[].Ebs.VolumeId]' \
--output table
What to do: for anything stopped longer than a month, create an AMI (which keeps the disks as snapshots, at snapshot prices), then terminate the instance. You can launch it again from the AMI if someone asks.
6. No budget, so nobody notices
None of the above is dramatic on its own. It adds up because nothing tells anyone. A budget with alerts is free and takes five minutes, and it is the difference between finding this once and finding it every month.
Console: Billing and Cost Management › Budgets › Create budget. A cost budget for the monthly amount you expect, with two alerts: 80% of the actual spend, and 100% of the forecast. Send them to a team address, not one person.
CLI (to see whether one exists):
aws budgets describe-budgets --account-id 123456789012 \
--query 'Budgets[].[BudgetName,BudgetLimit.Amount,BudgetLimit.Unit]' --output table
What to do: if the command returns nothing, create the budget today. Then enable Cost Anomaly Detection on the account: it learns the normal pattern and emails you when a service suddenly costs more, which is how most of the mistakes above announce themselves.
Making it stick
Thirty minutes finds the waste that exists today. The same waste comes back next quarter unless something watches for it: tags that say who owns what, a lifecycle policy for snapshots, a budget alert that reaches the team, and a review of every region on a schedule.
That ongoing review is what Cloud Health Check does. It connects to your AWS accounts and Azure subscriptions with read-only access, runs every check in this guide and a few hundred more every week, ranks the findings by what they cost, and shows the fix for each one. You can see a sample report on demo data in a few minutes, with no cloud access required, and connect your own account when you are ready.
Questions about your own account? Email contact@parlak.us or try Cloud Health Check on sample data.