The (hidden) cost of using framework: Express vs Native HTTP servers in Bun
In the previous article, we’ve taken a look at the (hidden) cost of using framework (express as an example) in Node.js. In this article, we’ll check the same for Bun.
The web frameworks were invented / developed to make our lives easier. In Node.js world, one of the most popular web framework is express. With 23M weekly downloads, express is still running very strong even though it is more than 10 years old. The express framework makes our life easier. It has middlewares, router, automatic request/response body processing, etc. But is there a cost to pay for all the features that we may or may not be using at all?
In this article, we’ll compare a hello world HTTP server in native Bun with express. Let’s find out if we pay a performance cost for using a framework. If we do pay a cost, how much it is? Express is used as an example because it is very popular.
The code
First, let’s look at a very simple hello world app written using express (the code is taken from express home page):
const express = require("express");
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => console.log("Listening on", port));
Now, let’s write a very similar app in native Bun using Bun.serve API. We’d need to develop a minimal router that can check for request method and path. Also, there needs to be top level error handling that sends a 500 response if business logic encounters any kind of unhandled error. Here is the code of such an app in native Bun:
Bun.serve({
port: 3000,
fetch(req) {
try {
if (req.method !== "GET") {
return new Response(null, { status: 405 });
}
const pathName = new URL(req.url).pathname;
if (pathName !== "/") {
return new Response(null, { status: 404 });
}
return new Response("Hello world");
} catch (e) {
return new Response(null, { status: 500 });
}
},
});
Of course, the number of lines of code is more than express. This is because we’re building over the bare bones provided by Bun’s serve API.
The environment
The performance test is executed in the following environment:
- MacBook M1
- 16G RAM
- Bun v0.2.2
The tester is a very minimal C++ app that uses high performant and stable libcurl to make HTTP requests.
The results
Let’s run some tests and find out the hidden cost. We’ll run 1M requests for different concurrency levels. We’ll run for 1, 25, 50, 100 concurrent connections.
First, let’s see the comparison for 1 concurrent connection (a total of 1M requests):
Of course, we didn’t expect express to perform at par with native code. That’s just not possible with a feature rich framework. For sequential requests, Bun native processes about two times more requests per second when compared to express. Bun native uses very less CPU, however memory usage is about the same.
Next, let’s look at the results for 50 concurrent connections (a total of 1M requests:
At medium concurrency of 50 concurrent connections, native code is able to process four times more requests per second. The CPU usage still stays low, while memory usage is about the same.
Lastly, let’s look at the results for 100 concurrent connections (a total of 1M requests:
The same result as before. The RPS is about three times more with native code. The CPU is low, while memory is the same.
Express is and will remain popular in the future. With rich functionality, of course there is a price to pay. Nothing is free, after all. However, the price turned out to be way more than what we’d expected.
Thanks for reading! More articles on similar topics can be seen in the magazine: The JS runtimes.