How My Cloud Bill Dropped from $500 to $50 a Month, Here's the Story


You've probably heard this story a hundred times: broke developer opens his AWS bill, sees the number, nearly has a heart attack, then panic-migrates to something cheaper while crying a little. 😅
Well, this isn't that story.
Because when I first built this system, the $500-a-month bill wasn't a problem at all. The app had already crossed a million downloads across three regions. The owner had money, real money, and one instruction: "Make it unkillable. Don't let it go down. Don't worry about cost."
So I built him a fancy, all-the-bells-and-whistles setup. Then a few years later, he asked me to slim it back down. This is the story of how I cut that bill by 90% without losing a single user.
When Money Is No Object
Let me set the scene first, because in this story the context matters a lot.
When you're handling a million users and a boss who genuinely doesn't care about the bill, you don't pick the cheap option. You pick the safe one. You just use every ready-made service AWS sells, because every service AWS runs for you is one less thing landing on your shoulders at 3am.
But before talking about what's inside, there's one thing that wasn't up for debate: three regions were mandatory. The app served users in Europe, Asia, and America. And it wasn't my technical taste that decided this, it was the law. The rule is called data residency, meaning data has to be stored in the user's own country.
The European rule (GDPR) is the strictest, but the idea is the same everywhere. A European user's data has to sit on European servers, an American user's on American servers, and so on. You can't just funnel everyone into one cheap data center and call it a day. Personal data stays in its own country, full stop. That's why every part below exists three times over.
So here's what I was running. All AWS, all managed by them, spread across Europe (Stockholm), Asia (Singapore), and America (Oregon):
- EC2 + Auto Scaling to run the app, automatically adding servers when things got busy
- RDS PostgreSQL for the database, 7-day backups, with an automatic standby in case one server died
- ElastiCache Redis to hold temporary data so the app stays fast
- Load Balancer to spread out the traffic, plus automatic redirect to HTTPS
- S3 for file storage, encrypted
- Route53 for DNS
- ACM for SSL certificates that renew themselves
- CloudWatch, ten-plus alarms watching CPU, memory, errors, speed, everything
- Secrets Manager + SSM to store passwords and settings
- ECR to store the app images
On paper it looked great. Tough, self-healing, and all managed by AWS. Backups, updates, recovery if something died, all on them. A server dies, a new one pops up before I've even finished reading the alert.
And for the situation back then, serving a million people who expected the app to just work, it was the right call. Give me the same situation and I'd build it exactly the same way again.
This is the part people rarely talk about. An over-the-top system isn't a mistake. It's the right answer to a question that no longer exists.
Because the question changed.
The Phone Call
Fast-forward a few years.
The app is stable now. Growth has leveled off, healthy and predictable. The frantic, race-against-time energy of the early days is long gone. Then one day the owner calls with a new order, the complete opposite of the old one:
"It runs great. Now make it cheaper."
And just like that, every assumption I'd built that expensive system on flipped over.
Automatic standby servers? We hadn't had a region go down in years. Auto-adding servers when busy? Traffic was steady, so it almost never kicked in. Ten CloudWatch alarms? I glanced at maybe two. I was paying a premium to guard against disasters that, frankly, never came.
For a long time I'd been paying extra so AWS would handle everything, and for a long while it was worth it. But the moment the priority shifted to cost, instead of staying-alive-at-all-costs, that payment turned from "smart insurance" into "burning money".
So I opened the bill, line by line, and started asking the uncomfortable question about every service:
Do I actually need AWS to do this for me, or am I just paying them so I don't have to think?
The NAT Gateway alone, the thing that lets servers on the internal network reach the internet, cost more than the servers themselves. The Load Balancer had an hourly charge plus a per-request charge. The database ran about $25 per region per month, for a tiny size I could honestly run on a potato. 🥔
It piled up fast. And almost none of it was doing anything for me anymore.
Why Move to Hetzner
For years I'd seen developers, mostly in Europe, talking about Hetzner Cloud. A German provider, nowhere near as famous as AWS or Google Cloud, but with a cult following thanks to one thing: ridiculous price for the performance.
Picture this: a server with 2 processor cores and 4 GB of RAM for about €4 a month. Compare that to the AWS equivalent at roughly $17, and that's before you stack the load balancer, the NAT gateway, and the database on top.
So now the question was: can I rebuild everything that expensive system gave us, but on Hetzner, without losing what actually matters now?
I could. But the mindset had to completely change.
On AWS, you pay extra so everything's handled and you don't have to think. On Hetzner, you hold the machine yourself, so you have to think. For the early days, "don't have to think" was correct. But now, thinking was exactly what saving money required.
Confession: Why There's No Docker
Okay, before I show you the new setup, let me get ahead of the comment section. Because the second a developer sees this, the first thing they'll type is:
"Wait, no Docker? In 2025? Are you insane?"
The honest answer has two layers.
First, the practical reason: we needed to move fast. This wasn't a relaxed greenfield project, it was a live app with a million users and orders to cut costs right now. Every extra layer is one more thing to configure, one more thing to debug, one more thing standing between me and a clean move. When the clock is ticking, you ship the simplest thing that works.
But "we were in a hurry" isn't a good reason. So here's the real one, the one I actually believe:
Docker solves problems I no longer have.
Think about it, what does Docker actually give you? It separates many apps so they don't interfere on one server. It makes the exact same environment on any computer. It manages tons of apps at once when there are a lot of them. All of that is genuinely useful, if you have those problems.
But my new setup is one app, one server, one region. There's nothing to separate, it's the only thing on the box. As for the identical environment, that's handled by my setup script, which I'll get to. That script builds an identical server every time, from empty machine to running app, with no manual steps. I just don't need Docker to do it.
And on a 4 GB server, Docker isn't free. It eats RAM, it eats processing power, and it adds an extra step every time I want to peek at the error logs or fix something at 2am. On a server running a single app, Docker is just overhead for features I'm not using.
So this isn't me being lazy or cutting corners. It's the same lesson as moving off AWS, just applied to something smaller: don't pay for something you don't need.
Later, if this grows into many apps per server, or I need to manage lots of servers at once, I'll reach for Docker without a second thought. It's the right tool for that job. Just not for this one.
The point is: don't install something just because "maybe I'll need it later". Keep it simple now, add it later when you actually need it.
The New Setup: What I Actually Built
So here's the new setup. And I want you to feel how much calmer it is.
One server per region. That's the whole story. But remember, one per region is still mandatory. The data residency rule from earlier didn't disappear just because the provider changed. User data still has to be stored in its own country. So the first thing I checked on Hetzner wasn't the price, it was whether they had locations that let me keep European data in Europe, American data in America, and Asian data in Asia. Turns out they did. ✅
- Hetzner Cloud servers, small size (2 cores, 4 GB) in Europe, plus equivalents in Singapore and America
- Nginx to manage and forward traffic to the app, running on the same server
- PostgreSQL 16 for the database, with its data sitting on separate storage that survives even a full server rebuild
- Redis for temporary data, running right on the server
- Node.js 22 + PM2 to run the app, no Docker as discussed
- Cloudflare for DNS and SSL, its free certificate handles HTTPS
- GitHub Container Registry (GHCR) replacing ECR, free for storing the app with a token
- Loki + Promtail + Node Exporter, free monitoring tools that ship the logs to my own dashboard (Grafana)
And here's the part that makes people nervous:
Everything is installed automatically by a single 351-line script. It runs the first time the server boots. It installs everything, configures Nginx, sets up the database, starts the app, and locks down basic security. The whole system, one script, one server, zero clicks.
Someone's bound to ask: "So what if the server dies? There's no backup?"
Fair. Honestly, on AWS the automatic standby server was handled for me. And back when we had a million screaming users, that mattered a lot.
On Hetzner, I traded the automatic standby for a predictable bill and full control. The database storage stays safe even if the server is rebuilt, but if it dies there's no instant replacement. There are pros and cons to that, it's not a free lunch.
But here's the thing, for where this app is now, that choice is exactly right. The owner asked for cheaper, not for the 99.999% uptime guarantee we were never actually using.
The Free-Tier Grind Nobody Writes in the Tutorial
This is the part I most wanted to write, because nobody talks about it.
The pain didn't come from Hetzner. It came from Cloudflare's free tier.
A bunch of the security features I wanted turned out to cost money. The most annoying one: I couldn't use full encryption (Full SSL), which needs a special certificate from paid Cloudflare. So I ended up using a simpler mode (Flexible SSL), which means the last leg, from Cloudflare to my Hetzner server, isn't encrypted on that hop.
For some systems, that's an absolute no. But I dug into what it actually meant for my specific case, understood how far the risk went, and then designed a solution that's safe within those limits.
And that became a recurring pattern. Every time I hit a feature that cost money, I had to stop, research, and find another way to get the same result. There were genuine moments, usually 2am staring at an Nginx config stubbornly refusing to forward traffic, where I thought maybe I should just go back to AWS. 😩
But hitting those walls had a point: it forced me to understand my own system. On AWS, I paid so I wouldn't have to think. On the free tier, there's no escape hatch. You can't just click "enable feature". I had to learn.
And once I understood it, it stuck. The system is stable, and I know exactly how each part works, because I assembled every bit of it myself.
The Numbers
Here's the before and after, no rounding in my favor.
AWS (before):
- NAT Gateway, the data transfer cost was pretty hefty
- Load Balancer, hourly plus per-request
- Database, around $25 per region per month
- ElastiCache, extra monthly cost
- CloudWatch, more cost on top
- Total: $500+/month
Hetzner (now):
- 3 production servers + 1 server for tinkering, about €25–30/month, all in
- Cloudflare DNS + SSL, free
- GHCR, free
- Grafana + Loki on the tinkering server, free
- Total: ~$50/month
A 90% drop. And honestly, my dashboard now is actually better than CloudWatch ever was.
So What's the Point
The lesson isn't "AWS is bad", okay. AWS is incredible. If you've got strict regulations, fifty engineers, and a product that genuinely can't go down for even five seconds, then stay on AWS or Google Cloud. When I had a million users and unlimited money, AWS was the right answer, and I'd pick it again.
The real lesson is subtler, and it's the thread running through this whole story:
How you build a system is just a reflection of your situation at the time. When the situation changes, the right answer changes too.
The expensive system wasn't wrong. It was right, back then. An expensive, fully-managed system with backups everywhere is the right answer to "money's no object, never go down". A lean single-server system is the right answer to "make it cheaper, keep it stable". Same person, same app, but opposite answers, because the question flipped.
The mistake isn't building the expensive system. The mistake is sticking with the expensive system for years after nobody needs it or wants to pay for the upkeep anymore.
I went from "AWS handles everything" to "I handle everything", and the 90% cost cut was just a bonus from being willing to learn my own system from scratch. The free-tier grind, the features I couldn't click for free, fixing Nginx at 2am, all of that is what turned a bill into a lesson.
If you want me to go deeper on anything, whether it's the setup script, the Cloudflare config, the Nginx setup, or how I store the data, just say the word. I'm planning to write the full guide as a follow-up.
Now go open your own bill. Ask that uncomfortable question about every line. You might be surprised which things you've been paying a premium for, just to never use them. 🚀