Cloud cost optimization and billing gotchas
A cloud bill that's 10x normal rarely comes from the thing you'd expect. It comes from a NAT gateway quietly billing per gigabyte, a debug log stream nobody turned off, or an idle resource left running since a demo three months ago — and by the time anyone notices, it's already a five-figure surprise.
3 min read
Why cloud bills spike in ways nobody predicts
On-prem hardware has a fixed, visible cost — you bought the box. Cloud resources bill continuously and per-unit, often for dimensions that aren't obvious from the architecture diagram: data transfer, API request counts, per-GB processing fees on top of the "obvious" compute cost. The result is that cost bugs behave like silent production bugs — nothing crashes, nothing pages anyone, the system works exactly as designed, and the only symptom is a bill that's wrong weeks later.
The recurring offenders
NAT gateway data processing charges. A NAT gateway bills per GB processed, separately from its hourly cost — easy to forget when it was set up once for "private subnets need outbound internet" and never revisited. A service that pulls large files or talks to external APIs at volume through a NAT gateway can rack up processing charges far larger than the compute cost of the service itself.
Cross-AZ data transfer. Traffic between resources in different availability zones (even within the same region) is billed per GB in each direction. A chatty microservice architecture where every call between services happens to cross AZs — common when instances are spread across AZs for availability, which is otherwise the right call — can turn into a meaningful line item purely from internal traffic that never leaves AWS.
Unattached and idle resources. EBS volumes left behind after an instance is terminated, unused Elastic IPs, load balancers with no healthy targets, RDS instances kept "just in case" after a project ends — none of these send an alert when they're doing nothing. They just bill quietly, forever, until someone audits the account.
Log retention set to "forever." CloudWatch Logs, by default, retains log groups indefinitely unless a retention period is set. A verbose service logging at INFO or DEBUG in production, with no retention policy, accumulates storage costs that grow every month with no ceiling.
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/app"
retention_in_days = 30 # without this, logs are kept forever by default
}Lambda and API Gateway at high request volume. Serverless pricing is genuinely cheap at low-to-moderate volume, but the per-invocation and per-GB-second model means a sustained high-throughput workload can cost more than an equivalent always-on server. The "no idle cost" pitch inverts into "no cap" at high enough traffic.
Practical habits that prevent the surprise
Budgets and alerts before you need them. AWS Budgets (or the equivalent in any provider) can alert at a percentage of a monthly threshold, or on anomalous day-over-day spend. Set this up once, for real thresholds, before the first incident — not after.
Tag everything for cost allocation. A resource tagged team:payments, env:production lets you break the bill down by team and environment; an untagged sprawl of resources makes "why did the bill go up" an investigation instead of a dashboard query.
Right-size before you reserve. Reserved Instances and Savings Plans are genuinely cheaper than on-demand, but only for capacity you actually need — committing to a 1-year term on an oversized instance locks in the waste for a year. Right-size (or move variable workloads to auto-scaling groups) first, commit second.
Lifecycle policies on storage. S3 lifecycle rules that transition infrequently-accessed objects to cheaper storage tiers (Standard-IA, Glacier) after N days, and expire genuinely temporary objects, turn "storage grows forever" into a bounded, predictable cost.
Treat cost anomalies like production incidents. A sudden spend spike is a signal something is misbehaving — a retry loop, a runaway query, a misconfigured cron — just as much as an error-rate spike is. Cost Explorer's anomaly detection (or an equivalent) deserves the same on-call attention as a CloudWatch alarm, because it's often catching the same underlying bug from a different angle.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why can a NAT gateway become a surprisingly large line item on a cloud bill?
2. Why can cross-AZ data transfer between microservices become a meaningful cost, even though the traffic never leaves AWS?
3. Why should a CloudWatch (or equivalent) log group always have an explicit retention period configured?
4. Why should you right-size a workload before committing to Reserved Instances or Savings Plans?