The bill that grows while traffic stays flat is not a billing mystery. It is the default behavior of a cloud account. AWS charges for what exists, not for what gets used, and in most accounts things get created far more often than they get deleted. After enough cost reviews you stop looking for the one big mistake. There usually isn't one. There are forty small ones, each of them defensible at the time it was made.
Nothing gets torn down
Every launch leaves sediment. The load-test environment from the spring release is still running because someone might need it. The staging copy built for one client demo outlived the client. We regularly find accounts running four environments where the team can name two.
Compute is only part of it. Terminate an EC2 instance and its EBS volumes often survive, because delete-on-termination wasn't set when someone clicked through the console years ago. Snapshots of those volumes survive everything. A common mistake worth naming: assuming a stopped instance costs nothing. The instance-hours stop; the storage keeps billing. Stopped-but-not-deleted is where a surprising amount of budget quietly lives.
Then there is the storage that grows by design. CloudWatch log groups default to never-expire. S3 buckets collect build artifacts, load balancer logs, and database exports with no lifecycle policy, because a lifecycle policy is a decision and shipping was the priority. None of this appears as a spike on a graph. It appears as a slope.
One check that takes ten minutes in any account:
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[].{ID:VolumeId,SizeGiB:Size,Created:CreateTime}'
Volumes in the "available" state are attached to nothing. They are the easiest find in any review, and nobody has ever missed one after deletion.
Sized for a deadline, never revisited
Before launch, someone picks an instance size with plenty of headroom, because the deadline is real and an undersized database makes for a bad launch story. Reasonable call. The problem is that nobody comes back. Downsizing has an asymmetric payoff for the engineer doing it: succeed and nothing happens, cause a slowdown and it has your name on it. So oversized stays oversized.
Autoscaling minimums follow the same pattern. A marketing push or an incident happens, someone raises the minimum from two to ten "for now", and the incident channel moves on. A minimum is a floor under your bill. The group will scale up under load, but it will never fall below the number someone typed under pressure two years ago. We look at minimums early in every review because they encode old emergencies.
The network lines nobody models
NAT gateways bill per hour and per gigabyte processed. A private subnet full of containers pulling images from ECR, or batch jobs reading from S3, routes all of that through NAT and pays data processing on every byte. A gateway endpoint for S3 or DynamoDB removes that traffic for free. Interface endpoints for other services carry their own hourly charge, so at low volume they can cost more than the NAT traffic they replace. It is a calculation, not a rule.
Cross-AZ transfer is the other quiet one. Every gigabyte between availability zones bills in both directions, and chatty services notice. The standard advice is to spread everything across three zones, and for production that is usually right. For a development environment that could be rebuilt from Terraform in an hour, multi-AZ is resilience you are paying for and do not need. Single-AZ non-prod trades availability for cost, and for systems whose downtime nobody would notice, that is a good trade.
Commitments bought once
Savings Plans and reserved capacity get bought during a cost push, produce a satisfying drop, and are then never looked at again. Two things drift. Usage grows past the commitment, so the new marginal spend runs at on-demand rates while everyone assumes it is covered. Or the architecture shifts, and you are locked into paying for shapes you no longer run.
The sequencing mistake is the expensive one: buying commitments before cleaning up, which locks in a discount on waste. The other mistake is over-committing. A three-year all-upfront plan has the best rate and is the wrong instrument for a team mid-migration to containers, Graviton, or another region. Commitment trades flexibility for discount; a one-year, no-upfront plan is a smaller discount and often the honest amount of certainty. Re-evaluating coverage quarterly is unglamorous and works.
What changes when someone can see it
Every mechanism above survives for the same reason: nobody owns the line item. The bill goes to finance, which sees resource IDs and service names it cannot map to a team or a feature. The engineers who could map them never see the bill. Finance cannot ask a precise question, engineering never has to answer one, and the slope continues.
Tagging and per-team allocation do not delete anything by themselves. What they change is behavior. When a team sees its own spend as its own list of resources, the load-test environment gets a teardown date, someone finally questions the minimum of ten, and the orphaned volumes disappear without a ticket. Visible waste with a name on it gets fixed in a way an aggregate number never does.
An untagged resource is a cost nobody has agreed to. Tag everything, give each team its own view, and people will either claim what they see or delete it. Both outcomes are cheaper than not knowing.
The shape of this engagement, end to end, is in our cost-visibility scenario.