Most AWS waste isn't a bad architecture decision. It's a charge that nobody decided on: a default, a leftover, a setting nobody revisited. I work across 170+ AWS accounts, and these seven show up in almost every one of them.
Each section has what it is, why it happens, how to find it (read-only commands) and the fix. Prices are us-east-1 list prices; check your region.
1. Unattached EBS volumes
What: an EC2 instance is terminated, but its data volume had DeleteOnTermination=false (the default for volumes attached after launch). The disk stays, billed every month: about US$0.08/GB-month for gp3, US$0.10 for gp2.
Find them:
aws ec2 describe-volumes --filters Name=status,Values=available \
--query "Volumes[].[VolumeId,Size,VolumeType,CreateTime]" --output table
Fix: snapshot anything you're unsure about (snapshots are much cheaper per GB), then delete the volume. While you're there, convert remaining gp2 volumes to gp3: same or better performance, ~20% cheaper, no downtime.
2. Public IPv4 addresses
What: since February 2024, AWS charges US$0.005/hour for every public IPv4 address — attached or not. That's about US$3.60/month each. EC2 instances in public subnets, NAT Gateways, load balancers (one per AZ), and idle Elastic IPs all count.
Find them: the quickest view is the free Public IP Insights in VPC IP Address Manager. Or list Elastic IPs that aren't associated:
aws ec2 describe-addresses --query "Addresses[?AssociationId==null].PublicIp"
Fix: release idle Elastic IPs, turn off "auto-assign public IPv4" on subnets that don't need it, and put instances behind a load balancer or use SSM Session Manager instead of a public IP for access.
3. NAT Gateways in dev
What: a NAT Gateway costs about US$0.045/hour (≈ US$33/month) before a single byte goes through it, plus US$0.045 per GB processed. Many VPC templates create one per Availability Zone — three per VPC — and get copied into every dev and test account.
Find them:
aws ec2 describe-nat-gateways --filter Name=state,Values=available \
--query "NatGateways[].[NatGatewayId,VpcId,SubnetId]" --output table
Fix: in non-production, one NAT Gateway per VPC is plenty (you accept that one AZ failure stops outbound traffic for dev — usually fine). For sandboxes that rarely need outbound internet, consider none at all.
4. CloudWatch Logs set to "Never expire"
What: every new log group defaults to keeping logs forever. Storage is about US$0.03/GB-month — small per GB, but debug logs from years ago add up quietly, and nobody reads them.
Find them:
aws logs describe-log-groups \
--query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" --output table
Fix: set retention on every group (aws logs put-retention-policy --log-group-name X --retention-in-days 30), and set it in your Terraform/CloudFormation modules so new groups never default to forever. Keep long retention only where compliance requires it — and then consider exporting to S3 with a lifecycle rule.
5. S3 incomplete multipart uploads
What: when a large upload fails halfway, the parts already uploaded stay in the bucket. They don't appear in the console object list, but you pay for their storage — indefinitely.
Find them:
aws s3api list-multipart-uploads --bucket my-bucket \
--query "Uploads[].[Key,Initiated]" --output table
S3 Storage Lens also shows "incomplete multipart upload bytes" across all buckets.
Fix: one lifecycle rule per bucket, ideally added by default in your bucket module:
{ "Rules": [{ "ID": "abort-mpu", "Status": "Enabled", "Filter": {},
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 } }] }
6. EKS clusters on old Kubernetes versions
What: each Kubernetes version gets about 14 months of standard support on EKS. After that, AWS doesn't stop your cluster — it moves it to extended support, and the control plane goes from US$0.10/hour to US$0.60/hour. That's roughly US$73 → US$438 per cluster per month, 6× more, for the same cluster.
Find them:
for c in $(aws eks list-clusters --query "clusters[]" --output text); do
aws eks describe-cluster --name "$c" \
--query "cluster.[name,version,upgradePolicy.supportType]" --output text
done
Fix: delete clusters nobody uses (check workloads and owners first), and upgrade the rest one minor version at a time, non-prod first. The next post in this series walks through doing that safely.
7. S3 and DynamoDB traffic through a NAT Gateway
What: instances in private subnets talking to S3 or DynamoDB go through the NAT Gateway by default — at US$0.045 per GB processed. A gateway VPC endpoint for S3 or DynamoDB does the same job for free: no hourly charge, no per-GB charge.
Find it: check if your VPCs have gateway endpoints:
aws ec2 describe-vpc-endpoints \
--filters Name=vpc-endpoint-type,Values=Gateway \
--query "VpcEndpoints[].[VpcId,ServiceName]" --output table
Then compare with NAT Gateway "BytesOutToDestination" in CloudWatch. If a VPC has a busy NAT Gateway and no S3 endpoint, that's usually your answer.
Fix: add the gateway endpoints and associate them with the private route tables. In Terraform it's one aws_vpc_endpoint resource per service. Existing connections keep working; new traffic takes the free path.
Seeing them in Cost Explorer
The commands above work one account and region at a time. To see how much these seven cost across the whole organization, open Cost Explorer in the management (payer) account, group by Usage type, and filter for the last full month. Useful usage types to search for:
-
EBS:VolumeUsage.gp2/.gp3— compare with what's attached -
PublicIPv4:InUseAddressandPublicIPv4:IdleAddress -
NatGateway-HoursandNatGateway-Bytes -
TimedStorage-ByteHrsunder CloudWatch (log storage) -
AmazonEKSusage with "extendedSupport" in the name
Then group the same view by Linked account. In my experience, a few accounts — often old sandboxes and abandoned projects — hold most of the waste. That tells you where to start, and who to ask before deleting anything.
The pattern
None of these need a re-architecture. Most take an afternoon, and the real fix is the same each time: change the default in the module or template that creates the resource, so the waste doesn't come back next quarter.
A simple order that works:
- Run the seven read-only checks above in every account and region.
- Fix the defaults in your IaC first (log retention, lifecycle rules, endpoints, gp3, subnet IP settings).
- Clean up what already exists, snapshot first when in doubt.
- Put a monthly reminder in the calendar to run the checks again.
What's the strangest AWS charge you've found? I'd love to hear it in the comments.
Badereddine is a cloud and DevOps engineer in Canada who operates 170+ AWS accounts and 500+ EKS clusters day to day, and runs TERRAX, a small AWS cost and platform engineering practice.
The 45 checks I run first, in the order that saves the most, are in a free checklist: AWS Cost Audit Checklist.













