My personal finance app runs on what I’d call a “properly built” AWS stack: VPC, ALB, Fargate, RDS Postgres, CloudFront. Properly built turned out to mean properly billed — production was costing about $135 a month to serve a handful of households. One afternoon of deliberate changes brought the expected run-rate to about $57, with zero downtime. Here’s exactly what changed, what it cost in resilience, and the one deploy that went sideways.

First, honest numbers. These come from Cost Explorer and live resource state, not vibes:

ComponentBeforeAfterChange
NAT gateway (+ IPv4)$36.00$0deleted
Fargate (2 × 0.5 vCPU/1GB)$36.04$9.011 × 0.25 vCPU/512MB
ALB + public IPv4$23.40$23.40kept
Task public IPv4$0$3.65new (see NAT)
RDS instance (Multi-AZ t4g.micro)$23.04$11.52Single-AZ
RDS storage (50 GiB gp2)$11.50$4.00gp3
CloudWatch (Container Insights)~$14~$1.50disabled
Misc (Secrets, Route53, CF, S3)~$4~$4kept

The changes, in deploy order

1. Delete the NAT gateway. The biggest single line. Tasks moved from private subnets to public subnets with assignPublicIp for egress; the security group still only admits inbound traffic from the ALB. A public IP on a locked-down task is not the security downgrade it sounds like — inbound posture is identical, and $33/month buys a lot of forgiveness for the aesthetic loss. I considered VPC interface endpoints instead: four of them (ECR, CloudWatch Logs, Secrets Manager, plus S3) cost about $29/month — nearly the NAT itself — and still wouldn’t cover SMTP or external API calls. Deploy-order footgun: update the API stack first (move the tasks), then the network stack (delete the NAT). The other order fails on “export in use.”

2. RDS Multi-AZ → Single-AZ. Pre-revenue app, 14-day automated backups retained. The tradeoff is real and accepted: an AZ failure now means downtime until a restore instead of a ~60-second automatic failover. For a product with no paying users yet, paying $12/month for a hot standby was insurance against the wrong risk.

3. gp2 → gp3 storage. Online operation, and at 50 GiB, gp3’s baseline 3,000 IOPS is better than gp2. This one is free performance plus $7.50 a month — there is no tradeoff; just do it everywhere.

4. Right-size Fargate. Two weeks of metrics said the two tasks were loafing: CPU average 0.36%, max 1.8%, memory max ~2%. Two 512/1024 tasks became one 256/512 task. Single-task risk accepted: a crash means a one-to-two-minute gap while ECS replaces it, and deploys still overlap (100% minimum healthy, 200% maximum), so releases stay seamless.

5. Disable enhanced Container Insights. ~$13/month of per-container metrics I looked at approximately never. The service-level alarms run on free AWS/ECS metrics and kept working.

The deploy that went sideways

Here’s the embarrassing part that made the afternoon interesting. Live production had last been deployed from a feature branch — it carried a session-secret and some routing infrastructure that had never landed on main. My cost-optimization branch was based on main. First deploy shipped a task definition missing SESSION_SECRET; the container panicked on boot.

What saved it: ECS’s deployment circuit breaker. The new task never went healthy, the deployment auto-rolled back, and production never stopped serving. I ported the missing infrastructure forward as its own commit and redeployed clean. Two lessons for the price of one: keep the circuit breaker on always, and treat “what branch is prod actually running?” as a question worth answering before touching infrastructure.

What I deliberately didn’t do

  • Drop the ALB ($23/month, the biggest remaining line). CloudFront needs a stable origin, and Fargate task IPs churn. Rejected.
  • Fargate Spot — saves ~$6/month, but interruptions on a single-task service are a bad trade.
  • The dev environment — still running its own NAT, ALB, and RDS at ~$83/month, which is the next decision: redeploy it on the cheap configuration or destroy it and recreate on demand.

The meta-lesson: none of this required cleverness. It required reading Cost Explorer line by line, checking two weeks of utilization metrics, and being honest about which resilience features a pre-revenue product actually needs. The “proper” architecture wasn’t wrong — it was just sized for the company I don’t have yet.


The account-level guardrails that caught this in the first place — budgets, cost anomaly alerts, the works — are packaged as a CDK baseline kit you can deploy in minutes.