How much performance do you give up when you split an app into microservices? Everyone knows it’s “some” but I wanted an actual number.
So I built the same app twice, once as a monolith and once as four services, gave both the exact same $112 a month of servers, and load tested them until they broke.
TL;DR
The monolith held 60,000 users. The best microservices setup held 30,000.
Each request cost the services 2.2 to 2.6 times more CPU, even with everything on one box.
Giving each service its own box wasted about a third of the CPU. Sizing boxes to the measured load got gRPC from 19,000 to 30,000 users.
gRPC beat HTTP/JSON for internal calls every time.
Setup
The app is Conduit, a Medium clone with articles, feeds, profiles, follows, favorites, and comments. It's the same app from my cores video, written in Go with the standard library.
The microservices version has four services: users, profiles, articles, and comments. Each one owns its own tables. They share one Postgres cluster, but each service logs in with its own database role and can't read anyone else's tables. So when articles needs an author's name, it has to ask the other services for it.
I also made sure the split wasn't a strawman:
Calls are batched. A list of 20 articles makes one call for 20 authors, not 20 calls.
Connections are reused.
Login tokens are checked locally, with no extra auth call.
Services talk directly, with no proxy or service mesh in between.
Both versions passed the same 197 checks through the public API, and their responses matched byte for byte.
Every layout got $112 a month of app servers on DigitalOcean. That buys one 8-core box with 16 GB of RAM, or four 2-core boxes with 4 GB each. Nginx ran on its own 8-core box, and Postgres was a managed 16-core cluster. Both were shared by every test and never came close to their limit.

At this point you might be thinking “that’s expensive for an 8 core vm!!” I agree which is why I switched from Digitial Ocean to Synteq after this experiment.

Similar server for ~$20 month! They were also kind enough to give me some server credits for my experiments, and give y’all $20 dollars in free credits with my link to try this stuff for yourself. Use https://synteq.com/arjay to give it a shot!
Thank you Synteq for sponsoring! Back to the methodology.
Virtual users browse like real readers: they open the feed, read articles, check profiles, load comments, and sometimes write something. 1,000 users makes about 250 to 270 requests per second, sent from three separate load generator machines.
To pass, a layout had to hold its load for 5 straight minutes with p95 latency under 500 ms, p99 under 1 second, and under 1% errors.
Monolith: 60,000 users
On the single 8-core box, the monolith held 60,000 users, about 15,400 requests per second.
Before splitting anything, I ran four copies of the monolith on the four 2-core boxes, with Nginx spreading requests across them. That held 50,000 users, 17% fewer. Each small box pays its own operating system and networking overhead, and one of the four was a bit slower than the others.
So that's two baselines: 60,000 on one big box, and 50,000 on four small ones.
Four services, one box: 25,000 users
Next I split the app into four services but kept them all on the same 8-core box. That way only the code changes, not the hardware.
With HTTP/JSON between services, it held 25,000 users. Less than half.
Here's why. To list articles, the articles service needs each author's profile, and profiles needs the user's name from users. Every hop means encoding a request, sending it, decoding it, handling it, and doing it all again for the response. In the monolith that whole chain is a function call and one SQL join.

You can see it in the CPU. The monolith used 0.47 ms of CPU per request. The services used 1.20 ms, about 2.6 times as much.
It isn't the database. Postgres actually did less work per request for the services (0.26 ms vs 0.43 ms), because small indexed lookups replaced the joins. All the extra cost is in the app servers.
Switching to gRPC: 29,000 users
Then I swapped the internal calls to gRPC. The public API stayed HTTP/JSON, so only the traffic between services changed.
That got 29,000 users, and CPU per request dropped to 1.01 ms. Better, but still more than double the monolith. A faster protocol makes each hop cheaper, but the hops are still there.

One service per box: 18,000 users
This is how most people picture microservices in production: each service on its own machine. I gave each service one of the four 2-core boxes.
HTTP held 18,000 users and gRPC held 19,000. Remember, the four-copy monolith held 50,000 on these same boxes.
The CPU graphs showed why. In the HTTP run, the articles box was at 95% while the comments box sat at 52%. The services don't need the same amount of CPU, so giving each one an equal box leaves capacity idle on the wrong machine.
Rebalancing: 30,000 users
This is the point of microservices: you can scale each piece on its own. So I used the measurements to change the layout. Articles got a second copy on the comments box, and Nginx sent it a quarter of the article traffic. No new hardware, just the same four boxes arranged better.
gRPC: 19,000 → 30,000 users (+58%). Every box ran close to full, and it matched the services on the one big box.
HTTP/JSON: 18,000 → 20,000 users (+11%).

Most of the gap between HTTP and gRPC is CPU per request. In this layout HTTP used 1.27 ms and gRPC 0.91 ms, 28% less. gRPC was also much faster below the limit: at about 19,000 users on one service per box, p95 was 86 ms with gRPC and 366 ms with HTTP.
But even the best layout held half of what the monolith did on one box, and 40% less than the four-copy monolith.
What happens past the limit
Push the monolith past its limit and it just gets slower. At 64,000 users it still served about 15,500 requests per second, the same as at 60,000.
The microservices served fewer requests once they were overloaded. The rebalanced gRPC layout dropped from 8,154 requests per second at 32,000 users to 6,825 at 36,000. Every microservices layout did this, with HTTP falling a bit harder. If you run services, keep real headroom.
Results
Most users each layout held for 5 minutes:

Layout | HTTP/JSON | gRPC | vs monolith |
|---|---|---|---|
A: Monolith, one 8-core box | 60,000 | n/a | 100% |
B: Monolith × 4, four 2-core boxes | 50,000 | n/a | 83% |
C: 4 services, one 8-core box | 25,000 | 29,000 | 42–48% |
D: One service per box | 18,000 | 19,000 | 30–32% |
E: Boxes sized to load | 20,000 | 30,000 | 33–50% |
Each layout ran once, and scores are good to about 5%.
What about serverless?
A common answer is "just add more compute." So here's the math from the measured CPU costs. To serve 15,000 requests per second, about what the monolith did, you'd need roughly 7 busy cores for the monolith, 14 for the rebalanced gRPC services, and 19 for HTTP. Aim for 80% utilization and that's about 9, 17, and 24 cores.
That's an estimate, not a benchmark. But moving to Lambda or autoscaling doesn't remove the extra work. You just pay someone else to do it.
So why does anyone use microservices?
Because the benefits aren't about performance. They're about people.
If separate teams own search, billing, and recommendations, a clean service boundary lets each team build, test, deploy, and roll back on its own schedule, in whatever language fits. At companies like Amazon, where I spent years working on this kind of architecture, that can easily be worth the extra servers.
The catch is that you have to actually get that independence. If shipping one feature still needs coordinated changes across four services, you're paying the performance cost for nothing. The same goes for blast radius: in this app, users is needed by almost everything, so breaking it breaks most of the site anyway.
So what does this actually mean?
On equal hardware, a well-built microservices split served a third to a half of the monolith's users. The biggest part is a fixed tax on every request. How you place the services and which protocol you pick decide whether you lose another third on top of that.
If you do run microservices: measure CPU per service and size each one to it, use gRPC for internal calls, and leave headroom.
And if you're a small team, start with a modular monolith. Keep your code in clean modules with clear boundaries, and deploy it as one app. My guess is that as small teams do more and more with AI, this becomes the default for most new apps.
Have a good week!
Arjay.
P.S. If you want to chat with me directly or get advice on this stuff, the best way is through my private discord server included with Devmaxx. Try it free for 7 days at devmaxx.dev.