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.
That's 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. By the time someone opens Cost Explorer, the unattached volume has been billing for six weeks and the oversized instance has burned another sprint's worth of budget.
The good news: on accounts between $10K and $500K a month, waste is predictable. It concentrates in the same seven places. 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. The checks 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.
01Leftovers
Orphans
Unattached EBS volumes, old snapshots, unassociated Elastic IPs and idle load balancers, billing for nothing.
02Sizing
Oversized
EC2 and RDS sized for a launch-day guess, and non-prod environments running all week.
03Pricing and design
Overpaid
Stable compute on on-demand rates, and data transfer through NAT gateways and across AZs.
Why the monthly review breaks
Most teams already "do cost reviews". They miss the same things for structural reasons.
- Waste is created continuously and reviewed in batches. Every termination can orphan a volume, every launch can be oversized, every new service can add cross-AZ chatter. A monthly look sees each one weeks late.
- The bill is grouped by service, waste is grouped by resource. "EC2 up 12%" doesn't say which 14 instances, or whether it's usage or rate.
- Fixing needs context the reviewer doesn't have. Is that
m5.2xlargeidle or sized for a monthly batch job? The person who knows isn't in the meeting. - Commitments drift on their own. Savings Plans coverage falls as usage grows, with no alert unless you set a budget for it.
Start here: one unexplained increase, worked through
Before the seven sources, here's how to read the 18% from the opening. The numbers are illustrative. Name the possible readings first, because each predicts something different in Cost Explorer:
01Reading 1
Real growth
A new service or a customer launch. Predicts new resources with owners and matching usage.
02Reading 2
Waste
Things nobody uses. Predicts cost rising in usage types that do not track traffic.
03Reading 3
Pricing drift
The same usage at a worse rate. Predicts falling commitment coverage as usage grows.
Step 1: which service moved? Compare two months by service:
aws ce get-cost-and-usage \
--time-period Start=2026-05-01,End=2026-07-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE
EC2 compute is up $4,100 and "EC2 - Other" is up $2,300. RDS is flat. Nothing else moved much.
Step 2: usage or rate? Re-run for EC2 with --metrics UsageQuantity grouped by USAGE_TYPE. Instance hours rose 9%, close to traffic, so cost grew faster than usage. Run aws ce get-savings-plans-coverage for the same two months: coverage fell from 74% to 58%. That's reading 3. New usage landed outside the Savings Plan and billed at on-demand rates.
Step 3: what's in "EC2 - Other"? That bucket holds EBS, NAT gateways and data transfer. Grouped by usage type, the jump is NatGateway-Bytes and EBS:VolumeUsage.gp3. Neither tracks traffic. Reading 2.
Conclusion. No new product explains it. Roughly half the gap is coverage that didn't keep up with growth (#3 below), and half is waste in volumes and NAT traffic (#1 and #6). Twenty minutes in Cost Explorer turned "nobody can explain it" into three tickets with owners.
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 built for a migration two years ago, a volume that no longer exists.
Why it happens. Deletion feels risky and storage feels cheap, so nothing gets deleted. 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
Repeat per region. Orphans collect in the regions you stopped using.
Typical impact. Usually 1 to 3% of the total bill. Small, but it's the easiest money on this list: an unattached volume has no workload depending on it by definition. Snapshot first, then delete.
2. Idle or oversized EC2 instances
What it is. Instances whose average CPU sits in single digits for 30 days. Some are truly idle, like 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 rightsizing means a restart, restarts need a change window, and change windows need someone to care.
How to detect it. Pull 30 days of 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, use $(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ) for the start time.)
A common rule of thumb: average CPU under 10% and maximum under 40% over 30 days makes the instance a rightsizing candidate. Check memory too if the CloudWatch agent publishes it, since CPU alone misjudges memory-bound services. For fleet-wide analysis, enable AWS Compute Optimizer, covered in depth in the native audit guide.
Typical impact. Often 10 to 20% of EC2 spend. Dropping one size within a family roughly halves that instance's cost.
3. Reserved Instance and Savings Plans coverage gaps
What it is. Every hour of stable compute on on-demand pricing instead of a Reserved Instance or Savings Plan is an overpayment you chose. Savings Plans discounts run up to 72% against on-demand (AWS Savings Plans). Realistic savings on typical mixes land around 30 to 40%.
Why it happens. Commitment feels scary ("what if we migrate off this family?"), so teams under-commit, or bought RIs three years ago and never renewed. Meanwhile the baseline has been flat for two years.
How to detect it. Check 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 usually target 70 to 85%, keeping the rest on-demand for elasticity.
Typical impact. Often the largest item on this list: 10 to 25% of compute spend on accounts that never made commitments routine.
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 a month each, and since AWS started charging for all public IPv4 addresses, in-use ones bill too. An Application Load Balancer costs roughly $16 to $25 a month at zero requests, plus LCU charges.
Why it happens. Load balancers get created per environment, per experiment and per migration, and outlive all three. Elastic IPs get allocated "so the IP doesn't change" for instances terminated a year ago.
How to detect it. Unassociated Elastic IPs:
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{IP:PublicIp,AllocId:AllocationId}' \
--output table
For load balancers, list them with 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 on Business support or above.
Typical impact. Small in absolute terms, usually $50 to $500 a month, but pure waste with no removal risk, and a good signal of overall hygiene.
5. Over-provisioned RDS instances
What it is. The database version of #2, but worse, because databases get sized for a launch-day estimate that never arrived, and nobody touches a production database that works. A db.r5.2xlarge at 7% CPU with 40 connections is paying for headroom it will never use.
Why it happens. Fear. Resizing RDS needs a failover or a maintenance window, and the person who sized it has usually left.
How to detect it. Check 30-day CPU with the same get-metric-statistics pattern as EC2, using --namespace AWS/RDS and --dimensions Name=DBInstanceIdentifier,Value=your-db. Check DatabaseConnections and FreeableMemory too, because CPU alone understates memory-bound databases. Compute Optimizer also covers RDS and classifies instances as over-provisioned for you.
Typical impact. RDS is often 15 to 30% of a SaaS company's bill, and one size down on a Multi-AZ database saves twice, since both nodes shrink. Teams commonly find 20 to 40% savings within RDS itself.
6. Data transfer: 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 an hourly charge. Chatty microservices spread across AZs, or private subnets pulling container images and S3 data through a NAT gateway, quietly add 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 a 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 the 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, sum BytesInFromSource and BytesInFromDestination in the AWS/NATGateway CloudWatch namespace (metric definitions). Data processing is billed in both directions, so that sum is the volume you pay for. Then ask what that traffic is. S3 and DynamoDB traffic through a NAT gateway is the classic finding, and a free gateway VPC endpoint removes it (gateway endpoints).
Typical impact. Wildly variable: near zero for monoliths, 5 to 15% of the bill for chatty microservice architectures. The NAT-to-S3 pattern alone is often hundreds to thousands of dollars a 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) needs 60 of the week's 168 hours. Running it around the clock pays for 108 hours of nothing, roughly 65% of that environment's compute.
Why it happens. Nobody owns turning things off, and one broken Monday-morning startup ends the initiative.
How to detect it. If you tag environments, group Cost Explorer by that 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
Compare non-prod cost with what a 60-hour week would cost. If you don't tag environments, that's finding zero. Fix it first.
Typical impact. Non-prod is commonly 20 to 40% of total spend at mid-market SaaS companies, and scheduling it can cut that portion by half or more.
Manual review versus continuous detection
| Monthly manual review | Continuous detection | |
|---|---|---|
| Age of a finding | About 15 days on average, often more | Hours |
| Coverage | Whatever the reviewer checks that month | All seven sources, every region |
| Engineer time | Most of a day, every time | Review and approve only |
| When skipped | The month of a big release, which creates the most waste | Not skippable |
| Output | A spreadsheet and some tickets | Findings with evidence and a proposed fix |
The deeper difference is cadence. The checks are the same in both columns. What changes is how long waste bills before anyone sees it.
Why monthly manual reviews keep failing
Here's the uncomfortable part, and the reason "we do a cost review every month" isn't a strategy: waste regenerates continuously.
01Day 0
Waste starts
An instance is terminated and its data volume keeps billing.
02Day 15
Average age
With a monthly review, the typical finding is two weeks old before anyone looks.
03Day 30+
Review
Someone opens Cost Explorer, finds it, and files a ticket.
04Day 45+
Maybe fixed
If the ticket has an owner. If the review was not skipped for a release.
The seven sources aren't a list you clear once. They're processes producing waste at a steady rate. Every terminated instance can orphan a volume. Every deploy can resize the wrong thing. Coverage gaps reopen every time usage grows past your commitments.
Run the numbers on a $100K monthly bill with 20% addressable waste: each month of detection latency costs about $20K. The review takes an engineer most of a day, so it gets skipped the month you ship a big release, which is the month that creates the most waste.
The fix isn't reviewing harder. It's making detection continuous. Start by hand: part two walks through a full audit of all seven sources with native AWS tools. Then part three shows how CloudThinker Optimize runs the same checks every day through read-only access, with every remediation starting in Manual, waiting for your approval. Teams that work through the full list typically reduce their AWS bill by 30 to 50%.
Related reading
- Manual vs continuous rightsizing
- Optimize: continuous cloud cost optimization
- CloudThinker × global AWS FinOps cost optimization
- Kubernetes cost optimization: 6 ways your cluster leaks money
Start with the free tier with 100 premium credits and no card required, or continue with the hands-on native audit guide.
