Performance Benchmarking: Bun vs. C# vs. Go vs. Node.js vs. Python
Every once in a while, it can be useful to benchmark several different languages or runtimes to see which perform the best. That's simply because things are always changing. Languages tend to get better over time and new alternatives are always just around the corner.
In addition to that, I have an interest in performance that stems from my work with OpenGL more than 20 years ago. I used to obsess over frame rates of my 3D applications. Back then I knew that performance was paramount in 3D development. However, I want to focus on web development for the sake of this experiment. And web development, contrary to 3D development, often has slightly different priorities. In web development we're looking for stability, security, performance, and ease-of-use. Web developers are always looking for the perfect language and framework that allows you to meet all of those goals. And I've chosen these languages because they've become popular choices for web developers and they meet these goals to varying degrees.
Now, it's important to note that in many cases the language you choose won't have a large impact on your real-world performance. That's simply because the bottlenecks are usually with IO. That is, third-party network requests, reading and writing files, querying databases, etc. On the other hand, the underlying performance of the language you're using plays a bigger part when you're dealing with very large amounts of traffic as you'll likely want to optimize every facet of your architecture.
I also want to point out that Bun is currently in beta. It's aiming to be a drop-in replacement for Node.js and it shows promise. However, the results here should be taken with a grain of salt as things will likely change again by the time Bun is ready for production.
Here are the goals of these benchmarks:
- Realism. I want the work being done to mimic a real-world scenario. It's inspired by one of my websites that is currently live and serving over 1,000 users per day. Each application contains two REST endpoints which query for data that came from Food Data Central. But in order to test the runtime and not a 3rd party dependency, the JSON data is loaded into memory when the application first starts up. This part of each application is excluded from the benchmark results. Then here's where the benchmarking begins: the load tests hit endpoints that get data by hash table lookup or with a binary search. The data is then returned as JSON.
- Fairness. Each runtime performs the same task and returns the same data. For simplicity each runtime is using HTTP 1.1 and gzip is disabled. The other thing I want to point out is that .NET is cross-platform now. So, .NET and all of the runtimes listed are running in Linux with the help of Docker.
The runtimes
Bun Canary (beta)
C# (.NET 7.0.5) with Minimal API
Go 1.20.3 with FastHTTP
Node.js 20.0.0 with Fastify
Python 3.11.3 with Flask
The setup
Each application runs one at a time in Docker. Docker has been configured to only use a single core so that the load testing tool won't starve resources for the Docker container. The load testing tool is called oha which is written in Rust and is very efficient. The results that were recorded were the best results for each benchmark out of 3 runs.
The machine
Macbook Pro
2.6 GHz 6-Core Intel Core i7
16 GB 2667 MHz DDR4
The HTTP requests
- /food/321358 - Returns full JSON data for Hummus.
- /search/nutrients/1005:0.1-0.1 - All food ids with only 0.1 Carbohydrates (should be no matches).
- /search/nutrients/1005:0.5-200.0 - All food ids between 0.5 and 200 Carbohydrates.
The code
https://github.com/SWortham/benchmark-fooddata
Benchmark results
See the spreadsheet of benchmarking results.
Rankings (according to requests per second)
Bun - 1st place (tie)
Go - 1st place (tie)
C# - 3rd place
Node.js - 4th place
Python - 5th place
Breakdown
Bun and Go were each very close to each other in terms of requests per second. Go was faster with request #1 but Bun was faster for request #2 and #3. Also, Go was easily the overall winner when it comes to memory usage. And strangely, Bun had the highest memory usage by a long shot. Of course, that may improve as the Bun team works towards a production release.
C# was not far behind. It performed admirably and memory usage was reasonable. It was interesting to note, however, that when I experimented with running these applications with 4 cores, C# memory usage went up to 162 MB, whereas Go remained at around 25 MB. I have seen similar results in other benchmarks. Go is remarkably memory efficient for a garbage collected language.
Now we start seeing a more significant difference with the last two languages. Node.js was about 2.4 times slower for request #1. But request #2 was and #3 weren't too bad. They were 38% slower and 55% slower than Go, respectively.
Python was by far the slowest. It was more than 4 times slower than Go across the board.
Conclusion
I chose these languages specifically for these reasons:
- I feel that it's easy to build backend APIs for the web in each of these languages.
- They are each garbage collected languages.
- They each have momentum behind them with a growing number of developers and companies choosing these languages all the time.
So, which should you choose for your next project? That's harder to say. Perhaps the biggest influencing factor is how effective your team can be with developing in your language of choice.
Certainly, don't choose Bun yet as it's still in beta. But remember that the Bun team has their sights set on backwards compatibility with Node.js. So, the good news there is that if you choose Node.js you could potentially have an easy upgrade path to Bun in the future.
Go is the most attractive option here if you're only looking at speed and memory usage. But whatever you choose, now at least you're equipped with the knowledge of just what kind of performance disparity you can expect with these languages.