Their AWS bill had been growing 15% quarter over quarter for 18 months. By the time they called us, they were spending $380K annually — for a B2B SaaS company with 12 engineers and maybe 400 active customers. Nobody on the team had explicitly decided to spend that amount. It had just... happened, one right-sized instance at a time, one data transfer charge nobody noticed, one dev environment that stayed up over Christmas and never came back down.
Ninety days later, they were at $200K annualized. Here's exactly what we found and how we cut it, without touching a single product feature or degrading reliability.
The audit: what $380K was actually buying
Before we could optimize anything, we needed to know what was running and why. AWS Cost Explorer tells you what you're spending; it doesn't tell you whether any of it makes sense. We pulled a full inventory with aws ce get-cost-and-usage broken down by service, region, and tag — or rather, by the absence of tags, which was the first problem.
About 60% of their infrastructure had no cost-allocation tags. Workloads that "belonged" to the payments service, the analytics pipeline, and the dev environment were all lumped together. Nobody could tell you with confidence which team owned what. The first week was mostly just tagging — unglamorous, necessary, and the prerequisite for everything that followed.
Once we could see by workload, the breakdown was roughly:
- EC2 + RDS: $210K/year — compute for production, staging, and three persistent dev environments
- Data transfer: $68K/year — mostly cross-region traffic nobody had intended to create
- S3 + CloudFront: $42K/year — reasonable for their data volume, but with an S3 Intelligent-Tiering misconfiguration
- Everything else: $60K/year — a long tail of NAT gateway charges, CloudWatch Logs retention, unused Elastic IPs, and orphaned snapshots
The wins, in order of impact
1. Right-size and reserve compute: -$94K
The production cluster was running on-demand instances sized for peak traffic — which, for a B2B SaaS with predictable working-hours usage, meant 70% of that compute sat largely idle nights and weekends. We ran CloudWatch metrics for actual CPU and memory utilization over 30 days. The finding: most instances were at 15–25% average utilization. Peak, at 55%.
We dropped one instance class (m5.2xlarge → m5.xlarge for the API tier), moved to auto-scaling groups with aggressive scale-in policies, and purchased 1-year Compute Savings Plans for the baseline capacity that clearly wasn't going away. Savings Plans at ~35% discount over on-demand for committed spend, on-demand for the burst tail.
The three persistent dev environments were the larger fix. They'd been created for specific projects, used for a few months, and forgotten. Nobody deleted them because nobody wanted to be the person who deleted something and broke a colleague's work. We created a auto-shutdown Lambda that scales non-production environments to zero at 8pm weekdays and keeps them off weekends unless an engineer explicitly opts out with a tag. The environments stayed available; they just stopped running when nobody was using them.
2. Fix cross-region data transfer: -$41K
This one took two days to find and twenty minutes to fix, which is the most frustrating kind of problem. Their analytics pipeline was pulling data from an RDS instance in us-east-1 into a processing cluster in us-west-2. That's not a design decision anyone made — an engineer had spun up the processing cluster in the wrong region during a prototype and the prototype had become production. Cross-region data transfer runs $0.02/GB. For their data volume, that was $3,400/month.
We moved the processing cluster into the same region as the database. One afternoon of work, $41K/year in savings.
The secondary cause of data transfer charges was NAT gateway traffic. Their application pods in private subnets were routing all outbound API calls through a NAT gateway — including calls to AWS services like S3 and SES that could be reached via VPC endpoints at zero cost. Adding S3, SES, and DynamoDB VPC endpoints eliminated roughly 30% of their NAT traffic.
3. Audit the orphaned resource tail: -$28K
Every AWS account that's been running for more than a year has a graveyard of orphaned resources. The pattern is always the same: an engineer creates a snapshot, an EBS volume, or an Elastic IP for a specific purpose, that purpose ends, and the resource keeps accruing charges forever because deleting infrastructure feels risky.
We ran a Prowler scan for unused resources and found:
- 847 GB of EBS snapshots older than 90 days with no corresponding running instance — $82/month
- 14 unattached Elastic IPs at $3.60/month each — $50/month
- CloudWatch Logs groups with indefinite retention (many app teams had set no expiration policy) — $1,200/month once we set 30-day retention on non-compliance logs
- 6 load balancers with no registered targets — $16/month each
- An unused ElastiCache cluster, still running, from a caching experiment 18 months ago — $340/month
We cleaned these up and wired the Prowler scan into the weekly ops review. An orphaned resource report in a Slack channel means the next graveyard doesn't accumulate quietly for 18 months.
4. S3 Intelligent-Tiering fix: -$17K
S3 Intelligent-Tiering automatically moves objects between storage tiers based on access patterns — a reasonable choice for unpredictable access workloads. The catch: Intelligent-Tiering has a monitoring fee per object, and it doesn't activate tier transitions until an object is at least 128 KB. For small objects, you pay the monitoring overhead but never get the tier-transition savings.
Their largest S3 bucket held hundreds of millions of small objects — thumbnail images, log fragments, JSON event payloads. All enrolled in Intelligent-Tiering. The monitoring charges exceeded what Standard-IA would have cost for the same access pattern. We migrated the small-object buckets to Standard-IA with a lifecycle rule to Glacier after 90 days. The per-object monitoring charges disappeared.
What we didn't do
A few things that look like easy wins on a cost report but aren't:
- We didn't aggressively spot-instance the production workload. Spot instances can save 70-90% over on-demand but require stateless, interruption-tolerant workloads. Their API tier wasn't designed for sudden instance termination. Saving $40K and introducing a P95 latency spike during spot interruptions isn't a good trade for a B2B product.
- We didn't consolidate databases to save on RDS licensing. They had three small RDS instances that, from a cost standpoint, could theoretically be consolidated. But each served a different service with different maintenance windows and failover requirements. Consolidating to save $400/month would have created a shared failure domain. Not worth it.
- We didn't fight the S3 data retrieval costs on the archival tier. Some cost lines are cheap for a reason — the data is cold, retrieval is slow, and the application handles that correctly. Optimizing the wrong things is its own kind of waste.
The outcome
- AWS spend: $380K → $200K annualized (47% reduction)
- Savings: $180K/year, from 90 days of work
- Reliability impact: None — no incidents during the optimization, no feature degradation
- Side effect: The tagging exercise gave them a clean cost-allocation model for the first time. Finance could now attribute infrastructure spend to product lines, which unlocked a meaningful conversation about which services were actually profitable.
"We had no idea we were spending $68K a year on data transfer. The whole optimization paid for itself in the first month and we didn't have to change anything users touch."
The pattern we see everywhere
Cloud bills grow in the dark. Engineers make reasonable local decisions — leave the dev environment up this week, create a snapshot before the migration, spin up that extra capacity before the product launch — and nobody steps back to look at the aggregate. By the time the bill is large enough to be conspicuous, there are years of accumulated decisions to untangle.
The fix isn't a one-time cleanup. It's building the visibility and accountability that makes the next 18 months different: cost-allocation tags enforced in CI, a weekly Slack report on unusual spend spikes, lifecycle policies on everything, and an auto-shutdown policy for non-production environments that requires an explicit opt-out rather than an explicit opt-in. That's the infrastructure that keeps the bill from growing back.