Nobody blows their AWS budget on EC2. Compute is the line item everyone watches, discusses in planning, and right-sizes when it grows. The overspend almost always comes from somewhere else — a handful of services that grow quietly because no single person ever decided they should.
Here are the five I see most often, why each grows unnoticed, and what to actually do.
1. NAT Gateways
A NAT gateway lets resources in a private subnet reach the internet. It bills two ways: an hourly charge simply for existing, and a per-gigabyte charge for every byte processed.
The hourly charge is small and predictable. The data processing charge is where budgets go to die, because everything leaving a private subnet passes through it:
- Container image pulls on every deployment and every autoscaling event
- Package downloads during CI builds running inside the VPC
- Traffic to AWS services like S3 and DynamoDB that could have used a VPC endpoint and bypassed NAT entirely
- Operating system updates across every instance
The fix is mostly VPC endpoints. A gateway endpoint for S3 and DynamoDB costs nothing and removes that traffic from your NAT path entirely. Interface endpoints for other services have their own hourly cost but frequently come out well ahead if the traffic volume is meaningful.
The second fix is architectural: question whether a workload genuinely needs to be in a private subnet reaching out through NAT, or whether it was placed there by a default template nobody revisited.
2. Data Transfer
This is the one that never appears in an architecture diagram, which is exactly why it surprises people.
AWS charges for data movement in several directions: between availability zones within a region, out to the internet, and between regions. Traffic within a single availability zone is generally free, which creates a specific trap.
You deploy across two availability zones for resilience — correct, standard practice. But now your application servers in one zone are talking to your database in another, and every query and every result crosses a billable boundary. A chatty microservice architecture can generate substantial charges purely from internal communication.
What to do:
- Keep tightly-coupled, high-traffic components in the same availability zone, and use zone redundancy at the tier level rather than making every call cross zones
- Put a CDN in front of anything serving significant volume to the internet — CDN egress is typically cheaper than direct egress, and you get caching as a bonus
- Be deliberate about cross-region replication; it is easy to enable and easy to forget you enabled
3. Orphaned Storage
Storage waste accumulates because deletion is manual and nobody owns it.
Three specific offenders:
- Unattached EBS volumes. You terminate an instance, the volume was not set to delete on termination, and it persists — billing monthly, attached to nothing, named after a project that shipped a year ago.
- Snapshots outliving their volumes. Snapshots are independent resources. Automated backup policies create them on a schedule, and unless the policy also expires them, they accumulate indefinitely. This is the version of storage cost that is hardest to trace, because the volume it relates to no longer exists.
- S3 without lifecycle rules. Logs, build artefacts, user uploads and database dumps all land in S3 and stay in the standard storage class forever. Objects nobody has read in two years cost the same as objects read constantly.
The fixes are unglamorous and effective: a lifecycle policy on every bucket that transitions old objects to cheaper storage classes and expires what should not persist; an expiry rule on every snapshot policy; and a scheduled job that reports unattached volumes so somebody has to look at the list.
4. CloudWatch Logs
You pay to ingest logs and you pay to retain them. Both charges are modest per gigabyte, which is precisely why nobody notices until the line item is significant.
The pattern is consistent. There is an incident. Someone raises the log level to debug to diagnose it. The incident resolves. The log level stays. Meanwhile the log group's retention setting was never configured, so the default is to keep everything forever.
Two years later you are paying to store verbose debug output from an outage nobody remembers.
What to do:
- Set an explicit retention period on every log group — thirty days covers the overwhelming majority of debugging needs
- Export anything needed for compliance to S3, where long-term storage is dramatically cheaper than CloudWatch
- Audit log levels in production and treat "we raised it during an incident" as a temporary state with an owner
- Check what your application logs per request; a stack trace on every handled error at high traffic adds up quickly
5. Idle Load Balancers and Forgotten Environments
Every load balancer bills hourly whether or not it has healthy targets behind it. So does every NAT gateway, every Elastic IP not attached to a running instance, and every provisioned database.
These accumulate through entirely reasonable behaviour. A staging environment for a feature that shipped. A load balancer for a service that was consolidated. A proof-of-concept from a client project that concluded. Each was created deliberately and none was deliberately kept.
The practical answer is a recurring audit rather than a clever tool. Once a quarter, list load balancers with zero healthy targets, unattached Elastic IPs, and databases with no connections over the last thirty days. The list is usually short and the savings are usually immediate.
Tagging helps enormously here, but only if enforced. A tag policy requiring an owner and an environment on every resource turns "what is this and can we delete it" from an archaeology exercise into a lookup.
How to Approach an Audit
If you have inherited an account and want to know where you stand:
- Open Cost Explorer, group by service, view the last six months. You are looking for lines that trend upward without corresponding growth in traffic or customers. That divergence is waste, not scale.
- Check the three usual suspects — load balancers with no healthy targets, unattached EBS volumes, log groups with no retention policy. These take fifteen minutes and frequently pay for the whole exercise.
- Set a billing alarm at a threshold that would concern you, so the next surprise arrives as an alert rather than an invoice.
- Then look at compute. By all means right-size instances and consider savings plans — but do it after the items above, because those are the ones growing without anyone's decision.
The Underlying Point
Cloud cost problems are rarely the result of a bad decision. They are the result of no decision at all — defaults that were never revisited, resources that outlived their purpose, retention settings nobody chose.
Which is good news, because it means the fix is usually a quarterly hour of attention rather than an architectural change.
Want a second pair of eyes on your infrastructure spend? See how I approach cloud work or get in touch.

