A 10-point AWS cost audit you can run today
Earlier this year we wrote about what we found after auditing 10 AWS accounts: between 25% and 40% of every bill was waste. That post reported the findings. This one is the checklist we used to find them.
Every check below is something you can run yourself, today, with read-only credentials. Each one comes with the actual CLI command and a note on what to look for. None of them require a FinOps tool, a vendor, or a re-architecture. Block a half day, work through the list, and you'll know exactly where your money is going — and where it's leaking.
The list is ordered deliberately. Checks 1 through 3 are pure cleanup — deletions with no risk and no discussion needed. Checks 4 through 8 are right-sizing and architecture, where the real money is. Checks 9 and 10 are about locking in the gains. Resist the urge to skip straight to Savings Plans; committing to spend you haven't right-sized yet is how teams end up paying a discount rate on waste.
1. Find unattached EBS volumes
When an EC2 instance is terminated, secondary volumes without DeleteOnTermination set stay behind — and keep billing. At $0.08/GB-month for gp3, a forgotten 500 GB volume is $40/month for storing nothing anyone reads.
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[].{ID:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime}' \
--output table
What to do: anything with status available is attached to nothing. Snapshot it if you're nervous, then delete it. We routinely find 200 GB to 2 TB of orphaned volumes per account — $16 to $160/month gone for zero effort. While you're looking at volumes: any that are still gp2 should be migrated to gp3. It's roughly 20% cheaper for the same or better baseline performance, and the migration is a single modify-volume call with no downtime.
2. Find old EBS snapshots with no lifecycle policy
Snapshots are cheap individually ($0.05/GB-month) and expensive in aggregate, because nothing deletes them by default. Automated backup scripts from 2023 are still faithfully producing snapshots of volumes that no longer exist.
aws ec2 describe-snapshots \
--owner-ids self \
--query 'Snapshots[?StartTime<=`2026-01-05`].{ID:SnapshotId,Vol:VolumeId,Started:StartTime,Size:VolumeSize}' \
--output table
What to do: anything older than six months with no compliance reason to exist should go. Then set up Data Lifecycle Manager so this never accumulates again — it creates and expires snapshots on a schedule, and it's free. The worst account we audited had 4 TB of snapshots dating back 18 months: $200/month.
3. Find unassociated Elastic IPs
An Elastic IP that isn't attached to anything costs $0.005/hour — $3.65/month each. Small, but they accumulate: every terminated instance, every abandoned NAT experiment leaves one behind.
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{IP:PublicIp,AllocID:AllocationId}' \
--output table
What to do: release anything that comes back. We routinely find 5 to 10 per account. It's not the $30/month that matters — it's that unassociated EIPs are a reliable signal that nobody is cleaning up, which means the bigger items on this list are waiting for you too.
4. Find idle and over-provisioned RDS instances
The most expensive item in almost every audit. Someone picked an instance size on day one and nobody revisited it. Pull two weeks of CPU data and let the numbers decide:
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name CPUUtilization \
--dimensions Name=DBInstanceIdentifier,Value=<your-db-instance> \
--start-time 2026-06-21T00:00:00Z \
--end-time 2026-07-05T00:00:00Z \
--period 86400 \
--statistics Average Maximum
What to do: if peak CPU never crosses 30% over 14 days, drop an instance class — each step down roughly halves the cost. Check FreeableMemory the same way before you commit. And while you're in there: Multi-AZ on a dev database is pure waste. We found a db.r5.xlarge at 12% peak CPU that a db.t4g.medium replaced comfortably: $350/month down to $55.
5. Check what your NAT gateways are processing
NAT gateways charge $0.045 per GB processed, and every byte leaving your private subnets flows through them — ECR pulls, log shipping, S3 uploads, third-party API calls. Measure it:
aws cloudwatch get-metric-statistics \
--namespace AWS/NATGateway \
--metric-name BytesOutToDestination \
--dimensions Name=NatGatewayId,Value=<nat-gateway-id> \
--start-time 2026-06-05T00:00:00Z \
--end-time 2026-07-05T00:00:00Z \
--period 2592000 \
--statistics Sum
What to do: divide the sum by 1e9 and multiply by $0.045 — that's your monthly data processing bill per gateway, on top of $32/month each in hourly charges. Remember to run it per gateway: if you have NAT in three AZs, you have three of everything. If the total is more than a few hundred GB, add VPC endpoints: a gateway endpoint for S3 (free), interface endpoints for ECR, CloudWatch, and STS. This typically cuts NAT traffic by 60-80% and it's 15 minutes of Terraform. In one audit this single check explained $370/month for a six-microservice team.
6. Find load balancers with no healthy targets
Every ALB costs a minimum of $16/month whether it serves traffic or not. The ones fronting services that were decommissioned last year are still billing. List them, then check target health:
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[].LoadBalancerArn' --output text | tr '\t' '\n' | \
while read lb; do
echo "== $lb"
aws elbv2 describe-target-groups --load-balancer-arn "$lb" \
--query 'TargetGroups[].TargetGroupArn' --output text | tr '\t' '\n' | \
xargs -n1 -I{} aws elbv2 describe-target-health --target-group-arn {} \
--query 'TargetHealthDescriptions[].TargetHealth.State' --output text
done
What to do: a load balancer with zero healthy targets is $16+/month for routing requests to nowhere — delete it. For the ones that are alive but barely used, consolidate: one shared ALB or NLB fronting an ingress controller with host-based routing replaces eight service-specific ALBs. We've seen $300-500/month in ALB sprawl in a single account.
7. Find idle EKS/ECS clusters and oversized node groups
An EKS control plane costs $73/month at zero workloads. Every "staging-old" and "test-cluster-2" in your account is paying that for a Kubernetes API that answers to nobody. Run aws eks list-clusters and justify each entry. Then, for the clusters that should exist, check how full the nodes actually are:
kubectl top nodes
What to do: delete idle clusters outright. For live ones, if nodes are sitting at 15-20% CPU and memory, your node groups are oversized — set proper resource requests, let the autoscaler consolidate, and move stateless workloads to spot. Halving a six-node group of m5.large is around $210/month. Same logic applies to ECS services with more tasks than traffic.
8. Check S3 for missing lifecycle policies
S3 waste hides in three places: buckets with no lifecycle policy, versioned buckets silently accumulating unbounded old versions, and incomplete multipart uploads you're billed for but can't see in the console. Find the buckets with no policy at all:
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | \
while read b; do
aws s3api get-bucket-lifecycle-configuration --bucket "$b" >/dev/null 2>&1 \
|| echo "NO LIFECYCLE: $b"
done
What to do: every bucket holding logs, backups, or build artifacts needs a lifecycle rule — transition to Infrequent Access or Glacier, expire what you'll never read. Versioned buckets deserve special attention: without a noncurrent-version expiration rule, every overwrite keeps the old copy forever, and a bucket that "holds 50 GB" can be billing for 400. Add AbortIncompleteMultipartUpload everywhere too (7 days is a sane default) — failed multipart uploads sit invisibly and bill until aborted. Enable the free tier of S3 Storage Lens to see where the bytes actually live.
9. Check for on-demand compute that should be committed
If your baseline compute has been stable for months and you're paying on-demand rates, you're leaving 30-40% on the table. AWS will tell you exactly what to buy:
aws ce get-savings-plans-purchase-recommendation \
--savings-plans-type COMPUTE_SP \
--term-in-years ONE_YEAR \
--payment-option NO_UPFRONT \
--lookback-period-in-days SIXTY_DAYS
What to do: commit only to your steady-state floor, not your peak — a one-year, no-upfront Compute Savings Plan covering the baseline is the low-risk default. Do this check after checks 4 and 7, though. Right-size first, then commit. Committing to over-provisioned capacity locks the waste in for a year.
10. Find untagged resources
You can't manage what you can't attribute. If resources aren't tagged with a team, project, or environment, your Cost Explorer breakdown is a single unhelpful bar labelled "EC2-Other."
aws resourcegroupstaggingapi get-resources \
--query 'ResourceTagMappingList[?length(Tags)==`0`].ResourceARN' \
--output text
What to do: tag everything that comes back — Tag Editor in the console handles bulk edits, and your Terraform provider's default_tags handles the future. Then activate your tags as cost allocation tags in the Billing console; they don't show up in Cost Explorer until you do. This check saves nothing by itself. It's what makes every future audit take an hour instead of a day.
How to run this
Block a half day. Run the commands against your production account with read-only credentials — every check above is safe to execute as-is. Put the findings in a spreadsheet with one column that matters: estimated monthly dollars. Sort descending, fix the top three, and schedule the rest.
Don't try to fix everything in one pass. In our experience the top three items usually cover 70% of the recoverable money, and momentum matters more than completeness. A $40/month Elastic IP cleanup feels productive but it isn't the point; the point is the $350/month database running at 12% CPU.
Two more habits make the next audit cheaper. First, re-run the checklist quarterly — the graveyard grows back, because creation is easy and cleanup never happens on its own. Second, turn on Cost Anomaly Detection in the Billing console. It's free, and it turns "why did the bill jump 30%?" from a forensic exercise into an email you got the week it started.
If your results look like ours, expect the total to land between 25% and 40% of your bill. That was the consistent pattern across all 10 accounts we audited — accounts running standard web stacks, not exotic GPU fleets.
Don't have the half day? We run this exact audit as a fixed-price service — you get a ranked report of every leak and the Terraform to fix it. Get in touch.
Vishwaraja Pathi
Cloud & DevOps specialist with 13+ years of experience. Founder of Adiyogi Technologies. Previously at Roku, Rocket Lawyer, and BetterPlace.