How To

AWS Cost Optimization: 7 Places Your Bill Is Leaking (and How to Find Them)

Your AWS bill grew 18% while traffic grew 6%. This guide shows how to split an unexplained increase into growth, waste and pricing drift in Cost Explorer, then covers the seven places AWS bills leak, each with a real CLI or console check and its typical cost.

4 min read
Cover Image for AWS Cost Optimization: 7 Places Your Bill Is Leaking (and How to Find Them)

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.

The seven leaks in this guide, grouped by why they bill. Each comes with a command you can run today.

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.2xlarge idle 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.

Three readings of an unexplained increase. Cost Explorer can separate them in about twenty minutes.

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 reviewContinuous detection
Age of a findingAbout 15 days on average, often moreHours
CoverageWhatever the reviewer checks that monthAll seven sources, every region
Engineer timeMost of a day, every timeReview and approve only
When skippedThe month of a big release, which creates the most wasteNot skippable
OutputA spreadsheet and some ticketsFindings 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.

Detection latency is the cost driver nobody budgets for.

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

Start with the free tier with 100 premium credits and no card required, or continue with the hands-on native audit guide.

awscostoptimizationfinopscostopsebsec2rightsizingsavingsplans