The Cloud Bill We Were Afraid to Open — and How We Cut It by $500 a Month
Every team has one: the invoice you scroll past a little too fast. Ours had crept up for two years, one "we'll clean that up later" at a time, until nobody could say with a straight face what half of it was for. This is the full story of the week we finally opened it — and how, thread by thread, an AI assistant pulled a rack's worth of servers down to a single box, deleted every load balancer, tiered our storage, migrated our databases, and quietly took more than $500 off the monthly bill — without a single site ever blinking.
It started with a number nobody wanted to look at
Cloud sprawl is insidious because every piece of it was, once, a reasonable idea. A server for an experiment that shipped. A load balancer for a launch that stuck. A static IP here, a forgotten backup there, a CDN distribution for a landing page nobody visits. None of it feels wasteful the day you create it. It's only a year later, staring at the total, that you realize you've been paying rent on a storage unit full of furniture you forgot you owned.
And it never gets cleaned, because deleting things in production is how outages happen. So the safe move is always to leave it — and the bill just climbs. We handed the whole mess to an AI with one rule: find everything, fix what's provably safe, and prove nothing broke. Step one was a brutally honest inventory.
| What was running | Scale | Why it bled money |
|---|---|---|
| App servers | A dozen+ | One per app, most idle, billed 24/7 |
| Load balancers | Several | Base fee + capacity units + data, even idle |
| Static IPv4 addresses | A dozen+ | Now billed per address, attached or not |
| CDN distributions | 70+ | Many orphaned, pointing at dead origins |
| Object storage | Hundreds of GB | All premium tier, incl. years-old files |
| Old deploy bundles & logs | Thousands of objects | Accumulating for years, never expired |
Seeing it laid out was almost a relief. It wasn't a mystery anymore. It was a to-do list.
Thread 1: a dozen servers, a dozen bills
We started where the money was. Every app lived alone on its own machine, behind its own load balancer — a dozen people each renting a whole house to sleep in one room. The fix is obvious and terrifying: put them together, and pray they don't step on each other.
So the AI built something sturdier than a shared folder:
Each app runs as an isolated process with a strict memory cap, so a greedy neighbor can't starve the rest. A lightweight reverse-proxy doorman reads a tag the CDN stamps onto each request and routes it to the right app — and because the CDN overwrites that tag every time, a visitor can't forge their way into the wrong room. The origin only answers requests carrying a secret handshake header from the CDN, so a machine with a public address behaves as if it were private — no NAT gateway, no VPC plumbing, no extra cost. One more subtlety the AI respected: apps that open a single global database connection can't share one process (the last connection silently wins), so those each got their own process. Thirteen apps, four codebases, one machine.
Thread 2: the load balancers had to go
With the apps under one roof, their load balancers pointed at nothing — and load balancers are a special kind of expensive: an hourly base fee, plus capacity units that scale with traffic, plus data processing, awake or asleep. Here's the detail that surprised us: one balancer isn't one address. It plants a node in every availability zone, each with its own static IP — so a single high-traffic balancer was quietly sitting on four billable IPs.
The AI played detective before deleting: which DNS records point here, which distributions use it as an origin, has it served a real request in 30 days? A couple were only "healthy" by luck — their apps were failing the health check (returning a redirect where the check demanded a plain 200) and the balancer's fail-open behavior was quietly covering for them. Nothing legitimate depended on any of them, so several always-on balancers dropped to zero.
Thread 3: one elastic IP to rule them all
Once everything ran on one origin and the balancers were gone, the whole operation needed exactly one static IP — where before there'd been more than a dozen. Cloud providers now bill every public IPv4 address, attached or not (~$3.60/month each), so this wasn't rounding error. The AI released each freed address only after confirming it was unattached on all three axes — no instance, no association, no network interface — a guard that caught addresses that looked free but were still bound to a balancer's hidden interface.
Thread 4: the storage quietly eating $20 a month
The biggest bucket had swollen to about 850 GB — mostly old generated media, the digital equivalent of attic boxes: rarely opened, impossible to throw out. All of it on standard storage at premium rates. The math is almost embarrassing:
The magic word is instant. There's an even cheaper deep-freeze tier, but it makes you wait hours to retrieve a file — useless if an app might serve it. The instant-retrieval tier costs a fraction of standard and still answers in milliseconds, so nothing broke. The AI worked in two passes — delete the junk, tier the rest — and it respected the fine print that burns naive scripts:
| The fine print | Why it matters |
|---|---|
| 128 KB minimum per object | Tiny files cost more after moving — so transitions were size-gated (nearly half the objects in one bucket were below it) |
| 90-day minimum duration | Never archive anything already slated for deletion |
| Transitions are one-way | You can't auto-promote data back, so the tiering had to be right the first time |
| Small transition fee per object | Millions of tiny moves add up — another reason to size-gate |
In deletions alone it cleared roughly 142 GB of files over a year old (old audio renders and upscaled images no longer referenced), and in one bucket it moved 78,858 objects to archival in a single automated run.
Thread 5: the orphans in the CDN
The content-delivery layer had grown to 70+ distributions, and many were ghosts — pointing at origins that had been deleted long ago. But you can't delete a distribution just because you don't recognize it; something might call it directly. So the AI proved each one dead three ways: its origin no longer resolved, it had served zero requests in 30 days, and no DNS record pointed at it. Only then did it disable each, wait for the change to propagate worldwide, and delete — along with the dangling DNS records that pointed nowhere.
It applied the same broom to the debris that never shows up in a glance at the bill. The full tally of quiet cleanup:
| Cleanup | How much | The guard rail |
|---|---|---|
| Orphaned CDN distributions | 17 removed (76 → 59) | Dead origin + 0 requests/30d + no DNS, then disable→wait→delete |
| Old deployment bundles | 879 pruned | Kept newest of each; never touched a version a live app ran |
| CDN access logs | 644,000+ objects / ~22 GB | Cleared, then set to auto-expire after 90 days |
| Log groups kept "forever" | 36 capped to 30 days | Stops the slow, invisible climb at the source |
| Storage deleted (> 1 yr old) | ~142 GB | Only unreferenced renders & upscales, past the retention window |
| Objects tiered to archive | 78,858 in one run | Size-gated above 128 KB; instant-retrieval so apps never wait |
Thread 6: the migration that almost shipped two-month-old code
Not every thread was tidy. The scariest moment came moving one of our busiest apps. The plan was routine — copy the running app to its new home, flip the switch. But when the AI compared the new setup against the live site before flipping anything, the pages didn't match: same app, wildly different sizes and content.
The server's saved startup config was pointing at an old build, while the live site was quietly running a version two months newer. A blind copy-and-cut would have shipped stale code to real users and called it done. Because the AI insisted on diffing the output before touching DNS, it caught the mismatch in seconds, found the correct build, redeployed, and re-verified. Nobody saw the old version. This one moment is the entire case for "verify before you cut over."
Thread 7: the database move that could have wiped live data
Higher stakes still: consolidating databases onto a shared cluster, where a copy running the wrong direction doesn't cost money — it erases data. Each migration copied every record and every index, then verified counts matched on both sides. Two subtleties a quick script would miss:
- Text indexes lie about their shape. A full-text search index reports its key as internal placeholders, with the real fields hidden in a "weights" property. The AI reconstructed them correctly instead of copying garbage.
- The listing hid a database. On one cluster, the account-level directory simply didn't show a database that was very much alive with 1,450 records. A lesser process would have read it as empty and overwritten the real thing. Before any wipe-and-replace, the AI fingerprinted both clusters — checking for signature databases unique to each — to be dead certain which was source and which was target. A moment of paranoia saved a live database.
Thread 8: the 66 GB ghost
One shared cluster looked like it had ballooned to 100 GB overnight; the fear was duplicated data. The AI chased it down and found something more interesting — not our data at all, but the database's internal replication log: a capped 1.65 GB log whose storage file had bloated to 66 GB from the churn of all our migrations, with the freed space never returned to disk. Actual data was a healthy, well-compressed ~33 GB with zero duplication. The proper fix was blocked by the managed provider even with admin rights — so the honest verdict was: it's cosmetic, it won't grow, leave it. Sometimes the smartest move is not pulling a thread. Knowing the difference is the whole skill.
Thread 9: changing the machine's DNA (and paying once, not twice)
To land the shared box on cheaper prepaid capacity, we had to move it to a different processor family — which isn't a resize at all. You can't grow a machine across chip architectures; code compiled for one won't even start on the other. It's a full rebuild. The machine's whole journey looked like this:
| Stage | Size | Why |
|---|---|---|
| Started | Oversized + 20 GB disk | Inherited, idle, wasteful |
| Right-sized | Smaller + 8 GB disk | Matched to real usage; disk can't shrink in place, so it was rebuilt |
| Re-homed | 2 GB RAM, new chip family | More headroom for all the apps, and it now runs on prepaid capacity |
So the AI did it the only safe way — the same five-step dance it used for every risky change:
The payoff was double: more memory and a machine now running on capacity we'd already paid for. Which raised the last thread — prepaid reservations sitting half-empty while other machines ran at full on-demand price. These reservations are size-flexible and counted in normalized units — a small machine is 0.25 units, the next size up 0.5, and so on — so the AI matched workloads to the pool we'd already bought instead of paying on-demand beside it.
| Prepaid capacity | Term | Status after cleanup |
|---|---|---|
| The shared origin box | Multi-year | Fully used — the whole fleet rides on it |
| A pool of small reserved instances | Two years remaining | Absorbed the consolidated workloads at no extra cost |
| One micro-instance family | Active | ~2.5 units idle — headroom for the next app, not a saving to bank |
That last row is the honest footnote: after all the consolidation, one family's reservation ended up partly empty. It's not waste we can refund — it's capacity already paid for, waiting for whatever we build next.
The part we didn't expect: it got faster, too
Cost work paired naturally with speed work, all of it free. By caching the app shell at the edge and inverting the caching rules — cache the static wrapper aggressively, never cache the dynamic, cookie-bearing responses — time-to-first-byte on the landing pages fell from roughly 500 ms to ~55 ms. Fonts were self-hosted as variable fonts (a single file spanning many weights) to kill render-blocking third-party requests. And basic hardening — rate limiting and security headers, correctly aware they sit behind a CDN — went in without paying for a managed firewall.
One honest note on the numbers
While auditing the bill, the AI found something worth saying out loud: our account runs partly on credits, which mask the true usage cost, and the billing dashboard's date windows didn't line up cleanly with reality. So it reconstructed costs from list pricing rather than trusting a misleading total — and corrected its own earlier estimate upward when it realized it had under-counted load-balancer charges. It also debunked a scare: "data transfer" looked alarming, but at ~10% of usage it was never the villain. Good cost work means being honest about what you can and can't measure.
Adding up everything we undid
| What changed | Before | After |
|---|---|---|
| Monthly bill | — | $500+ lighter · $6,000+/yr |
| Servers | A dozen+ | 1 |
| Load balancers | Several | 0 |
| Static IPs out front | A dozen+ | 1 |
| CDN distributions | 76 | 59 (live only) |
| Largest storage bucket | ~850 GB / ~$20 | ~280 GB / ~$3 |
| Edge time-to-first-byte | ~500 ms | ~55 ms |
| Downtime | — | Zero |
What we actually took away from it
The $500 a month is the headline, and it's real. But the part that stuck with us was how it happened. This was weeks of the careful, unglamorous work every team swears it'll get to — the auditing, the dependency-tracing, the nerve-wracking production cutovers — exactly the work that never gets done, because it's tedious and it's frightening. The AI wasn't afraid of the delete button, but it was never careless with it: it built beside, it proved, it flipped, it watched, and only then did it delete. And when a database's own listing lied, or a saved config pointed at stale code, it caught it — because it was checking, every single time.
We opened the bill we'd avoided for two years. A week later it was $500 lighter, running on a fraction of the machines, faster than before, and every site was still up. That's the quiet promise of AI-assisted operations: not a robot replacing your judgment, but a tireless one doing the exhausting, careful legwork your judgment never finds time for.