So yesterday I saw a post from a guy. He switched his backend from TypeScript to Golang, witnessed an unbelievable reduction in latency, and said switch to Golang today.
I decided to test things out for myself, and see where the choice of language or framework matters the most.
For this, I set up an experiment. I created an orders and a checkout api that makes database calls (I used postgres in this case), and I implemented the same logic in multiple languages, and in multiple frameworks within those languages, at the same time. Ten of them in all.
And I did not use a hello world endpoint for this, because that is not what we ship. The checkout does six reads, runs a discount engine with stacking rules and coupons, and then writes the order in one transaction of eleven statements that locks the stock before booking it.
One of those stacks is the Bun stack. I bet this is the first time some of you are seeing Bun stack, that you can write your entire backend in bun with zero dependencies. No node_modules, no lockfile, you just run the file and it serves. But that aside.
Before I timed anything at all, I made all ten of them return the exact same response, byte for byte, on the same inputs. That part matters more than the benchmark itself in this case, because if you do not do it, one stack can win by quietly doing less work than the others.
I spun up a docker container for postgres on my laptop and pinned it to its own cpu cores, so that the database, the api and the load generator are never fighting for the same core. I stopped every other container on the machine, and I set my cpu to performance mode and not power saving, so there will be no interference with my cpu during the test.
I then load tested the apis to see what they can do in this case, how well they perform in terms of max requests per second, and also how well they perform in terms of memory efficiency. There were 450 runs in total, 5 repetitions of every combination, 6 million api calls, 44.8 million sql statements, and zero errors.
Upon testing, rust came out on top on memory and on cpu per request. But on raw requests per second, rust and go are basically tied, and go fiber even beat every rust stack on the checkout, 1,597 against 1,438. So the difference between the languages is not so much, if you ask me, for proper daily use cases.
At 300 requests per second, which is about 26 million calls a day and more than most of us will ever serve, all ten of them sit within 1ms of each other on p99. At that load the language is not your problem in this case.
The latency we witness here is not in the language itself, but in the data layer, in how the data is handled in this case. Gorm makes 7 calls rather than 6 for this same simulation, because its preload goes and fetches the customer tier with a second select instead of joining it. And we can see its performance tallying that of bun on the read endpoint, 2,271 against 2,243, when the same go with hand written sql does 3,495. On the checkout it is even worse, 565 against bun's 1,048. Same language, same router, only the data layer changed.
However, where the tradeoff comes in is in terms of memory use. So your choice of language will come in really handy when you are talking of memory efficiency. Rust holds 8MB, bun holds 79MB, and node holds 243MB for the very same work. You can use a single contabo vps for 30 backends if you write them in rust, but for ts it can be just 3 that you can see in this case, as it consumes more memory to run and function. That is the real bill, and it is not latency.
That being said, your choice of language or stack should depend on two things. The compute resources you have available, and the skills you have on ground.
And if you engineer your data layer well enough, the orm you might just not need to add. Or perhaps you write your sql directly. You might just beat the matrix, trust me.
So do you need to change your backend to golang?
Vertical scaling might help you, and horizontal scaling might help you too. But understand what you are paying for in this case. Vertical scaling buys you a bigger box, and memory is the cheap part of that box. Horizontal scaling buys you more instances, and every single one of them carries that same footprint again, so the memory bill follows you across the whole fleet.
For this case, if what is hurting you is the memory and not the latency, then perhaps I think you should. That is the one thing you cannot really engineer away in ts. But if you can overcome it by adding ram, which is almost always cheaper than a rewrite, or by running fewer workers per instance, or by putting your one hot endpoint in another language and leaving the rest alone, then leave it where you are.
Because there are other factors that influence you making this work in this case. The skills you have on ground, how fast you ship, who maintains it after you, and how long a rewrite will freeze everything else you were supposed to be building. Those ones do not show up in any benchmark.
I hope I have been able to convince and not to confuse you as to why you either need or not need a backend rewrite.
For reproducibility, I have the methods and the results documented at the github repo:
github.com/hallelx2/polyglot…