Skip to content

From $24,000 to $2,000 a month on AWS: the three causes I found

Oversized instances, infinite loops and uploads through the backend raised no errors for months. How each one shows up, and what to check first on your bill.

4 min read5 sections
  • Sized wrongfound in utilization
  • Brokenfound in cost vs traffic
  • Built wrongfound only in the code

On a government identity project, I read the AWS bill before anything else. It came to $24,000 a month. Six months later the same system ran for $2,000 a month.

No part of the system had been decommissioned, and the product did the same work for the same users. The difference came from three separate problems, each of a different kind. The kinds matter more than the dollar figures, because each kind is found a different way.

Flexera’s 2026 survey of 753 organizations estimates that 29% of cloud spend is wasted, and 85% of those organizations name managing that spend as a top challenge. The bill I was reading was about eight times what the workload needed, which is far more than 29% too large. The final bill came in below that estimate, so the cut from $24,000 to $2,000 is twelvefold.

01

Kind one: something was sized wrong

Everything was over-provisioned, from the instances to the database.

This is the boring cause and the one everyone expects. It is rarely negligence. Someone sizes an environment during a launch, when no one knows the load yet, and picks generously, which is the right call at the time. Once the load is known, no one returns to the sizing: nothing is broken, and there is always something more urgent to do.

You find it by comparing provisioned capacity with observed utilization over a month. If your database sustains 8% CPU and never peaks above 25%, the spare capacity comes from a sizing decision that was never revisited.

An over-provisioned system goes unnoticed because it behaves perfectly. No alert fires and no user complains. The only signal is the invoice, and the invoice usually goes unread.

02

Kind two: something was broken

Infinite loops were running in the system and consuming compute continuously. They weren’t a hot path or an inefficient query, just loops that never exited, and someone had been paying for them month after month without knowing they existed.

This is a fault, and it is found a different way. Over-provisioning makes utilization graphs look low. A runaway process makes them look high, and the usage can’t be traced to anything users are doing.

You find it by checking whether cost follows traffic. If compute stays flat overnight while requests drop to near zero, something is running that serves no one.

24 hours · illustration, not data— traffic— cost
running for no one
If cost stays flat while traffic drops overnight, something is running that isn’t serving anyone.

The loops survived because the system worked: users got what they asked for, requests succeeded and no error was thrown. A loop burning CPU in the background doesn’t show up in any signal a team normally watches. You see it only when you ask why the cost curve stays flat while the traffic curve drops.

03

Kind three: something was built wrong

Uploads went through the backend when they could have gone straight from the client to object storage.

Every photograph of a face or a document arrived at the server, waited in its memory and was then sent on to storage. In this system every ID or passport application was an upload, so that path carried most of the traffic.

It costs in three ways. Each file holds instance memory until its request ends, so the instances are sized for the number of simultaneous uploads instead of for the work the application does. Each byte crosses the network twice, and one of the two trips didn’t need to happen. And the instance sets how many uploads the system can take at once, even though object storage can take far more. How to send uploads straight to storage is in a separate post.

This one doesn’t show in utilization or in the cost curve. You find it by reading the code and asking what the server does with each request. Because only the code shows it, it is the last of the three to be found, and usually the largest.

It survives because it is the default: the tutorial does it, the middleware documentation shows it, and it works perfectly at ten uploads a day.

04

Why no one noticed

The three causes were found three different ways, and none of them produced an error. Users complain about a slow endpoint, and a failing job sets off an alert. The over-provisioned instances, the loops and the upload path in this system all kept behaving correctly for months, and they were paid for on every monthly bill.

Cost doesn’t get noticed on its own. Someone has to go and look at it, on a schedule.

05

What I’d tell a team starting today

Set a budget alarm before you need one. It costs nothing, and infinite loops can only run for months when no one is watching the number. An alarm would have caught these in week one rather than week fifty.

Read the bill service by service, then line by line. The total tells you it’s too high, and the breakdown tells you which of the three kinds you’re looking at. Then check whether cost tracks traffic, because if it doesn’t, something is running for no one.

After that, read the code. The architectural causes don’t appear on any dashboard, and they are the expensive ones.

Where this came fromReading the AWS bill of a national ID and passport issuance system. The same project also produced an offline-first capture path for field agents working without connectivity.Read the case study →Related serviceCloud cost reviewView service →Next postA rollback in seconds let us migrate PHP to Node during the dayRead →

Writing

If you’re working on something similar, send us the details.

The first call takes 30 minutes, and you’re under no obligation.

Tell us about your projectcontact@microtech.consulting