Journal — August 21, 2025 · 4 min read
Why Your AWS Bill Is 3× What It Should Be
The specific line items that quietly inflate an AWS bill — idle capacity, NAT gateways, forgotten storage, and cross-AZ traffic — and how to find yours in an afternoon.
Almost every AWS bill I've audited had the same shape: a handful of line items nobody chose, quietly compounding. Not architectural mistakes — just things that got provisioned once and never revisited.
Here's where the money actually goes, roughly in the order I check.
1. You're paying for capacity you don't use
This is the big one, and it's rarely subtle. An m5.2xlarge running at 8% CPU costs the same as one running at 80%.
Open Cost Explorer, group by instance type, then cross-reference with CloudWatch CPU and memory over the last month. You're looking for anything sitting under 20% utilisation at peak. In most accounts that's a third of the compute.
The fix is boring: right-size down two sizes, watch for a week, repeat. Compute Optimizer will do the analysis for you for free and is right most of the time.
While you're there — check for instances nobody owns. Every account I've looked at has at least one EC2 box spun up for a demo in 2023 that's been running ever since. Tag everything with an owner, then hunt the untagged.
2. NAT gateways
This one surprises people, so it gets its own section.
A NAT gateway costs about $32/month just to exist, plus roughly 4.5¢ per GB of data processed. Run three of them for multi-AZ redundancy and push meaningful traffic through, and you're looking at hundreds of dollars a month for a component nobody thinks about.
Two things to check:
Do your private subnets actually need internet access? If the traffic is to S3, DynamoDB, ECR, or most other AWS services, a VPC endpoint routes it off the NAT entirely. Gateway endpoints for S3 and DynamoDB are free. This alone has cut NAT bills by 80% in accounts I've worked on.
Do you need one per AZ? In production, yes — a single NAT is a single point of failure. In dev and staging, absolutely not. One NAT, or none.
3. Storage nobody deleted
Storage is cheap enough per-GB that it never triggers alarms, and expensive enough in aggregate to matter.
- Unattached EBS volumes. When you terminate an instance, its volumes may survive. They keep billing. Filter EBS by state
available— anything listed is being paid for and used by nothing. - Old snapshots. Automated snapshots with no lifecycle policy accumulate forever. Set a retention rule.
- S3 without lifecycle rules. Logs and backups sitting in Standard when they haven't been read in a year. Move them to Infrequent Access or Glacier. A lifecycle policy is five lines of config and often the single highest-leverage change on this list.
- Incomplete multipart uploads. Failed large uploads leave fragments that are invisible in the console but very much billed. One lifecycle rule cleans them up permanently.
4. Cross-AZ data transfer
Data moving between availability zones costs about 1¢ per GB in each direction. That sounds trivial and isn't, if your app server in us-east-1a is chatting constantly with a database in us-east-1b.
You want multi-AZ for resilience — that's not the mistake. The mistake is unnecessary chatter across zones: a read replica in another AZ serving traffic that could be served locally, or a service mesh that routes without any zone awareness.
Check the "Data Transfer" line in your bill. If it's more than a few percent of the total, something is talking across a boundary it doesn't need to.
5. Environments that don't sleep
Your dev and staging environments almost certainly run 24/7. Nobody uses them nights or weekends.
A working week is 40 hours out of 168. Shutting non-production down outside business hours cuts those environments by roughly 70%. An EventBridge rule and a Lambda is an afternoon of work and it pays for itself the first month.
6. Logs
CloudWatch Logs bills on ingestion and storage, and log groups default to never expire.
Set a retention period on every log group — 7 days for debug, 30 for application, longer only where compliance demands it. Then check what you're logging at all: a service writing a line per health check every second is generating millions of useless events a month, and you're paying to ingest and keep all of them.
How to actually do this
Don't try to fix everything. Do this in an afternoon:
- Cost Explorer, group by service, last 3 months. Find your top five. That's where the money is — the rest is noise.
- For each of the top five, group by usage type. This turns "EC2: $4,000" into "EC2-Other: $1,200" and suddenly you know it's NAT gateways.
- Fix the top two. Ignore the rest for now.
- Turn on a budget alert so the next surprise arrives by email rather than at the end of the quarter.
The reason bills drift isn't incompetence. It's that nothing in AWS pushes back when you over-provision — there's no error, no warning, just a slightly larger number thirty days later. The only correction is a human deciding to look.
Put a recurring hour in the calendar. Monthly is plenty. That single habit is worth more than any tool you can buy.