AWS Cost Optimization: 7 Places Your Bill Is Leaking (and How to Find Them)
Your AWS bill grew 18% last quarter. Traffic grew 6%. Nobody can explain the gap.
If that sounds familiar, you're in the normal failure mode of AWS cost optimization: spend compounds quietly, in resources nobody is looking at, and the monthly cost review catches it 30 days after the meter started running. By the time someone opens Cost Explorer, the unattached volume has been billing for six weeks and the oversized instance has burned through another sprint's worth of budget.
The good news: on accounts between $10K and $500K/month, waste is remarkably predictable. It concentrates in the same seven places, every time. This guide covers each one — what it is, why it happens, how to detect it with a real command or console path, and what it typically costs. Run the checks today; they take about an hour.
This is part one of a three-part series. Part two is a hands-on AWS cost audit using only native tools; part three covers automating the whole loop with an AI agent.
1. Unattached EBS volumes and orphaned snapshots
What it is. When you terminate an EC2 instance, its EBS volumes are only deleted if DeleteOnTermination was set — and for secondary volumes, it usually isn't. The volume detaches, enters the available state, and keeps billing at full price. Snapshots pile up the same way: an old backup script, an AMI someone built for a migration two years ago, a volume that no longer exists.
Why it happens. Deletion feels risky and storage feels cheap, so nobody deletes anything. A gp3 volume costs $0.08/GB-month whether or not anything reads from it.
How to detect it. List every volume not attached to anything:
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime}' \
--output table
Then list snapshots you own that predate your retention policy (adjust the date):
aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[?StartTime<=`2026-01-01`].{ID:SnapshotId,Vol:VolumeId,Size:VolumeSize,Start:StartTime}' \
--output table
Remember to repeat per region — orphans love the regions you stopped using.
Typical impact. Usually 1–3% of the total bill. Small percentage, but it's the easiest money in this list: unattached volumes have, by definition, no workload depending on them (snapshot first, then delete).
2. Idle or oversized EC2 instances
What it is. Instances whose average CPU sits in single digits for 30 days straight. Some are truly idle — a proof of concept nobody shut down. Most are oversized: someone picked an m5.2xlarge "to be safe" and the service never needed more than an m5.large.
Why it happens. Capacity planning happens once, at launch, under uncertainty. Nobody revisits it, because EC2 rightsizing means a restart, and restarts need a change window, and change windows need someone to care.
How to detect it. Pull 30 days of average CPU for a suspect instance:
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 --metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
--start-time $(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 86400 --statistics Average Maximum
(On macOS, replace the start time with $(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ).)
A common rule of thumb: average CPU under 10% and maximum under 40% over 30 days means the instance is a rightsizing candidate. For fleet-wide analysis, enable AWS Compute Optimizer — we cover it in depth in the DIY audit guide.
Typical impact. Often 10–20% of EC2 spend. Dropping one instance size cuts that instance's cost roughly in half.
3. Reserved Instance / Savings Plans coverage gaps
What it is. Every hour of stable compute you run on on-demand pricing instead of a Reserved Instance or Savings Plan is money you chose to overpay. Savings Plans discounts run up to 72% versus on-demand (AWS pricing docs); realistic committed-use savings on typical mixes land around 30–40%.
Why it happens. Commitment feels scary — "what if we migrate off this instance family?" — so teams under-commit, or bought RIs three years ago and never renewed. Meanwhile the baseline load has been flat for two years.
How to detect it. Check your Savings Plans coverage for last month:
aws ce get-savings-plans-coverage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY
And RI coverage, if you use RIs:
aws ce get-reservation-coverage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY
Or in the console: Billing and Cost Management → Savings Plans → Coverage report. If coverage of your stable baseline is below roughly 70%, you're leaving discount on the table; mature teams typically target 70–85% coverage, keeping the rest on-demand for elasticity.
Typical impact. Frequently the single largest line item in this list — 10–25% of total compute spend on accounts that never systematized commitments.
4. Unused Elastic IPs and idle load balancers
What it is. An Elastic IP not associated with a running instance bills hourly (about $3.60/month each — and since AWS moved to charging for all public IPv4 addresses, in-use ones bill too). An Application Load Balancer costs roughly $16–25/month baseline even at zero requests, plus LCU charges.
Why it happens. Load balancers get created per-environment, per-experiment, per-migration, and outlive all three. EIPs get allocated "so the IP doesn't change" for instances that were terminated a year ago.
How to detect it. Unassociated EIPs:
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{IP:PublicIp,AllocId:AllocationId}' \
--output table
For load balancers, list them (aws elbv2 describe-load-balancers) and check RequestCount in CloudWatch for each — near-zero requests over two weeks means idle. Trusted Advisor's "Idle Load Balancers" check does this for you if you have Business support.
Typical impact. Small in absolute terms — usually $50–500/month — but pure waste with zero removal risk, and a good signal of overall hygiene.
5. Over-provisioned RDS instances
What it is. The database equivalent of waste source #2, but worse, because databases get sized for a launch-day load estimate that never materialized, and nobody touches a production database that's working. A db.r5.2xlarge running at 7% CPU with 40 connections is paying for headroom it will never use.
Why it happens. Fear. RDS resizes require a failover or maintenance window, and the person who sized the instance has usually left the company.
How to detect it. Check 30-day CPU on a suspect instance — the same get-metric-statistics pattern as EC2, with --namespace AWS/RDS and --dimensions Name=DBInstanceIdentifier,Value=your-db. Also check DatabaseConnections and FreeableMemory — CPU alone understates memory-bound databases. Compute Optimizer now covers RDS and will classify instances as over-provisioned for you.
Typical impact. RDS is often 15–30% of a SaaS company's bill, and one instance-size reduction on a Multi-AZ database saves twice (both nodes shrink). Teams commonly find 20–40% savings within RDS itself.
6. Data transfer costs: cross-AZ and NAT gateways
What it is. The line items nobody can read. Cross-AZ traffic bills $0.01/GB in each direction. NAT gateways bill about $0.045/GB processed plus hourly charges. Chatty microservices spread across AZs, or private subnets pulling container images and S3 data through a NAT gateway, quietly generate four- and five-figure monthly charges.
Why it happens. Data transfer is invisible at design time. Nothing in a Terraform plan says "this service mesh will move 40 TB/month across AZs."
How to detect it. In Cost Explorer, group by Usage Type and filter the usage type group to EC2: Data Transfer - Inter AZ. Via CLI:
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--filter '{"Dimensions":{"Key":"USAGE_TYPE_GROUP","Values":["EC2: Data Transfer - Inter AZ"]}}'
For NAT gateways, check the BytesOutToDestination metric in the AWS/NATGateway CloudWatch namespace, then ask what that traffic is. S3 and DynamoDB traffic through a NAT gateway is the classic finding — a free gateway VPC endpoint eliminates it entirely (AWS docs on VPC endpoints).
Typical impact. Wildly variable: near zero for monoliths, 5–15% of the bill for chatty microservice architectures. The NAT-gateway-to-S3 pattern alone is often hundreds to thousands of dollars per month.
7. Non-production environments running 24/7
What it is. Staging, dev, QA, demo, and load-test environments running nights and weekends. A business-hours workload (12 hours, 5 days a week) needs 60 of the week's 168 hours. Running it 24/7 means paying for 108 hours of nothing — roughly 65% of that environment's compute cost.
Why it happens. Nobody owns turning things off, and one bad experience with a broken morning startup ends the initiative.
How to detect it. If you tag environments, group Cost Explorer by your environment tag:
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=TAG,Key=environment
Then compare non-prod cost against what a 60-hour week would cost. If you don't tag environments, that's finding zero — fix that first.
Typical impact. Non-prod is commonly 20–40% of total spend at mid-market SaaS companies; scheduling it can cut that portion by half or more.
Why monthly manual reviews keep failing
Here's the uncomfortable part, and the reason "we do a cost review every month" is not an AWS cost optimization strategy: waste regenerates continuously.
Every terminated instance can orphan a volume. Every deploy can resize the wrong thing. Every new microservice can add cross-AZ chatter. Coverage gaps reopen every time usage grows past your commitments. The seven sources above aren't a list you clear once — they're processes that produce waste at a steady rate.
A monthly review means every finding is, on average, 15 days old before anyone sees it, and older still before anyone acts. Do the math on a $100K/month bill with 20% addressable waste: each month of detection latency costs roughly $20K. The review itself takes an engineer the better part of a day — so it gets skipped the month you ship a big release, which is exactly the month that creates the most waste.
The fix isn't reviewing harder. It's making detection continuous. Start manual — the next article in this series walks through a full audit of all seven sources using only native AWS tools — but know where this ends up.
Run these checks continuously
Everything above can run on autopilot. CloudThinker's CostOps agent connects to your AWS account through read-only access and scans all seven waste sources continuously instead of monthly, surfacing findings the day they appear — with remediation gated behind your approval. Teams that work through the full list typically reduce their AWS bill by 30–50%.
Start with the free tier — 100 premium credits, no card required — or continue the series with the hands-on DIY audit guide.
