A FinOps teardown of my own AWS bill
My day job is FinOps for a large AWS environment. So when my personal AWS bill, which runs eight side products, more than doubled in a month, I ran the same playbook on myself. Most of the jump wasn’t what it looked like, and the one real fix taught me something about “simple” rightsizing.
Step 1: Break the bill down before reacting
Total spend went from about $21 to about $47 month over month. Grouped by service, and then by usage type, October looked like this:
| Line item | Cost | Recurring? |
|---|---|---|
| Domain registrations (two new domains) | $33.98 | Yearly |
| Route 53 hosted zones (11 × $0.50) | $5.31 | Monthly |
| EC2 t3.micro (new this month) | $3.82 | Monthly |
| Public IPv4 address | $1.85 | Monthly |
| CodeBuild minutes | $0.83 | Per deploy |
| EBS gp3, 16 GB | $0.63 | Monthly |
Lesson one: the biggest line wasn’t a cost problem. Two domain purchases were 70% of the month. Registrar charges are lumpy: any month with a purchase or renewal looks like a spike. A forecast built on that month (AWS’s own forecast said $50) is wrong, because it extrapolates one-time charges.
Step 2: Find the real run rate
Strip out the one-time charges and annualize. The product behind the new EC2 line costs about $15 a month at steady state:
- t3.micro, 730 hours: $7.59
- Public IPv4: $3.65. Since 2024, AWS charges for every public IPv4 address, and for a tiny instance that’s about a quarter of the cost.
- 16 GB gp3: $1.28
- Builds, the hosted zone, and the domain spread over 12 months: about $2.50
Step 3: Fix the alerting before the spend
The product had a $1/month budget alert. Normal operation trips it every month, so it was pure noise, and noise is how real alerts get ignored. I raised it to $20, just above the run rate, so it only fires on something real: a runaway build loop, a bigger instance, a traffic surprise.
Step 4: The one real optimization, t3 to Graviton
A t4g.micro (ARM/Graviton) costs about 20% less than a t3.micro for the same vCPUs and memory. On paper that’s a one-line change. In practice, changing CPU architecture touched five things:
- The machine image. The CloudFormation template pinned the x86_64 Amazon Linux AMI. I made it conditional on the instance family.
- The Node.js download in the bootstrap script was hard-coded to
linux-x64. - The build environment. The app bundles native binaries (
sharpfor image processing), so CodeBuild must build on ARM too. - The release cache. Releases are content-addressed so unchanged code is never rebuilt. Without the architecture in the key, the deploy would have happily reused the x86 build on the ARM server.
- Parameter drift.
aws cloudformation deploykeeps a parameter’s previous value unless it’s passed, so changing the template default alone would have done nothing.
I previewed a change set first. It replaced only the instance, and the Elastic IP was modified, not replaced, so the address stayed the same.
Step 5: The bug the migration exposed
The first deploy failed: the new server’s first-boot setup finished 16 seconds after the deploy tried to install the app. The x86 server had always been booted long before any deploy, so nobody had noticed the race. The fix is one line: wait for cloud-init status --wait before installing. That gap had been there all along; replacing the server just made it show.
Results
- The steady-state cost of that product dropped about 20% on compute.
- The budget alert now means something.
- The deploy pipeline is architecture-aware and can’t install a mismatched build.
- The forecast confusion is explained: registrar charges are planned yearly, not treated as drift.
The takeaway
Don’t start a FinOps review by cutting. Start by classifying each charge as one-time or recurring, and as waste or the cost of running the product. Here the “spike” was planned spending, the noisiest problem was an alert, and the only real optimization was a small rightsizing that needed real engineering to do safely.
At enterprise scale the numbers have more zeros, but the method is the same.