journal // Sep 06, 2024
Indie Hacker Diaries #17: Scaling an Indie Hacker App
I mentioned in last week’s post that I had started doing some refactoring work on Push—the deployment service for Joystick apps that I’ll be launching after Parrot.
The goal of the refactor was simplicity. Over the last two years figuring out how to build the service, I learned a lot about what influences scale, how to measure production performance, and make decisions about infrastructure.
The big takeaway from what I learned? You need far less than you think. Most of your “bottlenecks” are from your code, not your infrastructure.
Now, I’ve known this for years. Back when I lived in Chicago a few years ago, I gave a talk at a meetup at Braintree when I was still working with Meteor on this exact topic. Funny enough, exactly one person in the audience got what I was talking about.
Paraphrasing:
Scalability isn’t a magical feature of a web framework. There are certain ways of doing things within a framework that can impact scale, but generally speaking, most of the responsibility for how well an app can scale depends on your code. How you implement features, how many (and what) dependencies you rely on, whether or not you’re properly indexing your database, etc.
That was true then and it’s true now. Still, the debates about whether a technology will scale happen. Every. Single. Day.
As I’ve solidified work on Push, this opinion has been further cemented. Getting my hands dirty with infrastructure and the dev ops side of things, it’s become clear that “scale” comes down to nothing more than how well and how efficiently you utilize the compute resources available to you.
Your language or framework can influence that—I’ve certainly discovered optimizations in Joystick as I’ve built everything out—but it’s not likely to be a show-stopper.
You know what’s also not a show stopper? Your actual scale.
Unfortunately, ego is one of those out-of-control features of the startup and indie hacking world. Everyone wants to be the bigger, faster, stronger, and better, almost to the point of absurdity. From the mountain tops, nerds thump their chests proclaiming their status as “King Shit of Fuck Mountain.”
Aspiring to that long-term is fine, but sadly, many (and I include a former self in this) get deluded into thinking they’ve already achieved those things before they’ve even launched.
You convinced yourself that you need to handle Google-level traffic when you haven’t even secured a single customer. You’re on a hot dog budget sniffing pictures of caviar, mate.
This line of thinking is pure poison. It leads to wasted time and money, building things and buying resources that you don’t need today and likely won’t need for a long time; if ever.
And that’s okay.
The question to ask yourself isn’t “how many people know about my thing,” but instead “do enough people know about my thing to make me a living? To validate my idea? To bring me the amount of freedom that I desire for my life?”
I’d argue that 1,000 people who love what you’ve built and are excited to support you is far better than a lukewarm sea of millions.
But I digress.
What I really want to answer is, “what sort of ‘firepower’ do I need for my indie hacker app?” To put it in practical terms, I want to share a quick test I did using some load testing tools the other night against a Joystick app deployed using the new version of Push.
Earlier this week, David Heinemeier Hanson (or, DH H), the creator of Ruby on Rails tweeted a hint about Basecamp’s infrastructure and what sort of traffic load they experienced. He suggested, at peak, they needed to handle 5,250 RPS (Requests Per Second).
Basecamp did 5,250 req/sec at peak yesterday. Mean response time was 90ms. So call that needing 500 cores at max load. If you skipped redundancy, you could probably do that with 3 boxes each running a Z5 192-core AMD chip with room to spare. Servers have gotten crazy fast!
— DHH (@dhh) September 4, 2024
I asked Claude to generate me a load test using autocannon
(an NPM package) that Basecamp example. Here’s the test it spit out:
autocannon -c 1000 -d 60 --renderStatusCodes --latency --rate 5250 <domain>
The important part there should read as “simulate 1,000 concurrent (simultaneous) users accessing my server, attempting to make 5,250 requests per second.”
I set up two types of servers:
- 8GB, 4VCPU (Premium AMD) - $56/mo.
- 192GB, 48VCPU (Premium Intel) - $1,814/mo.
For both tests, I rendered the default landing page for new Joystick apps via res.render()
. The app was setup in cluster mode to saturate all available CPU cores. For networking, Nginx runs a reverse proxy in front of the Joystick app (w/ SSL enabled).
For the first server, I got an RPS of 587 on average.
For the second server, I got an RPS of 1,105 on average.
A single instance. No load balancer or sibling instances to share traffic with. Zero caching or optimization. One box running raw, unoptimized code.
The worst outcome for both servers was that the longest request took about 10 seconds.
Those numbers may not seem impressive to you, but consider what that amounts to. That’s the traffic Basecamp sees at peak. Not an indie hacker app with a few hundred or thousand active users only making a few requests per minute.
That means that, if you wanted to, you could easily build and “scale” your indie hacker app—here, using Joystick and Push, or, arguably with any framework—to a profitable level with a single server.
Read that again. A single server.
Quick sidebar: I’m not stupid. I know this is a simple example and every app is different and has different requirements. The point I’m making is that you can, theoretically, achieve a lot with less than you think.
Now, this doesn’t cover resiliency (if your server goes down, your app is down), but I’ve been thinking lately...who cares?
If you achieve 99.999% uptime (known as “five nines” in dev ops circles), what do you think your downtime will be every year? I’ll tell you:
- 5.26 minutes of downtime per year
- About 25.9 seconds of downtime per month
- Approximately 6 seconds of downtime per week
Interesting. So, assuming your single instance achieves that level of uptime (which, barring any mistakes on the code/deployment side by yourself, should be possible), that means that at worst, your customers wouldn’t be able to access your app for—and I’ll round up here—six minutes per year.
Let that sink in.
You don’t need 10 servers. You don’t need autoscaling. You don’t need a zero-downtime deployment system. You don’t need to spend hundreds or thousands of dollars on hardware that will barely be utilized.
You certainly can, but at indie hacker scale, it’s overkill.
You just need one server, with a decent amount of CPU and RAM that works, running code that’s been thoughtfully optimized to the best of your abilities within the scope of your app.
Now that I’ve burst your bubble (or set you into a fit of rage), I’m going to leave you with a quote from the book “Ego is The Enemy” by Ryan Holiday:\
“When we remove ego, we’re left with what is real. What replaces ego is humility, yes—but rock-hard humility and confidence. Whereas ego is artificial, this type of confidence can hold weight. Ego is stolen. Confidence is earned. Ego is self-anointed, its swagger is artifice. One is girding yourself, the other gaslighting. It’s the difference between potent and poisonous.”
Kill your ego, spin up a single server, and hack, my dude.
Ryan