Is Bun really much faster than Node.js?

Mayank Choubey
Tech Tonic
10 min readSep 16, 2023

--

In the year since its initial release, Bun, the latest JavaScript runtime, has been laser-focused on delivering exceptional speed. Whether it’s application performance, unit testing efficiency, or build times, Bun consistently outpaces the competition. The one area where Bun decisively outperforms even the venerable Node.js is in terms of speed. Let’s take a look at some notable statements directly from Bun’s official documentation:

Bun is a new JavaScript runtime built from scratch to serve the modern JavaScript ecosystem. It has three major design goals:

  • Speed. Bun starts fast and runs fast. It extends JavaScriptCore, the performance-minded JS engine built for Safari. As computing moves to the edge, this is critical.
  • Elegant APIs. Bun provides a minimal set of highly-optimimized APIs for performing common tasks, like starting an HTTP server and writing files.
  • Cohesive DX. Bun is a complete toolkit for building JavaScript apps, including a package manager, test runner, and bundler.

Bun’s official graphs shows it to be about five times faster than Node.js:

Throughout our journey of evaluating Bun’s performance against Node.js, we’ve conducted a series of rigorous benchmarks. Initially, we were impressed with Bun’s lightning-fast execution, especially in simple use-cases such as a “Hello World” scenario. However, as we delved into more complex and demanding tasks, Bun’s initial advantage began to dwindle. For a comprehensive view of our past real-world benchmark results, feel free to explore them here.

With the long-awaited release of Bun 1.0, anticipation has been building, and many have inquired whether this version surpasses its predecessors in terms of speed. This article aims to answer those inquiries.

In this article, we will investigate whether Bun truly outperforms Node.js, potentially achieving a speed increase of up to fivefold. Our real-world test case involves assessing the performance of both platforms in generating QR codes through an API. Without further ado, let’s dive into the benchmark analysis.

Hardware and Testing Environment

The benchmarking tests were conducted on a MacBook Pro M1 boasting 16GB of RAM, ensuring a robust testing platform. To perform these assessments, the Bombardier test tool was employed with a custom modification enabling the transmission of random URLs within QR code requests. It’s crucial to emphasize that all URLs used during the tests were entirely unique, eliminating any possibility of repetition. Furthermore, the software stack utilized for these tests consisted of the most up-to-date versions available at the time of writing:

  • Node.js v20.6.1
  • Bun v1.0.2

Framework Selection and Rationale

Given the resource-intensive nature of QR code generation, the choice of web framework takes a backseat in this evaluation. However, we made sure to select the swiftest options available for both Node.js and Bun platforms.

In the case of Node.js, Fastify, renowned for its speed and efficiency, was the natural choice. It is widely recognized as the fastest web framework within the Node.js ecosystem.

On the Bun side of things, while Bun is typically positioned as a drop-in replacement for Node.js applications, it was found that Fastify applications didn’t function seamlessly on the Bun platform. As a result, we opted for the Elysia framework, a purpose-built solution designed and optimized specifically for Bun, ensuring the reliability and performance needed for our benchmarking tests.

In the realm of QR code generation, we’ve employed the qrcode framework across both Node.js and Bun, ensuring a consistent benchmarking approach. Qrcode is a widely acclaimed technology, boasting an impressive average of 1 million weekly downloads. Its unrivaled popularity sets it apart from any other QR code generation framework in the field.

That’s all about the setup. Let’s get started with the test. Before jumping to the QR API, we’ll first run a couple of quick tests on both frameworks to ensure that they can handle much more load.

Evaluating the frameworks

As previously discussed, our initial step will involve executing basic “Hello World” applications on both Node.js and Bun. This preliminary test aims to establish that the frameworks themselves do not serve as performance bottlenecks in low-performing scenarios. Let’s commence with the most straightforward “Hello World” scenario.

Hello world

Node.js

import Fastify from "fastify";
const fastify = Fastify({ logger: false });

fastify.get("/", (_, reply) => {
reply.send("Hello world!");
});

fastify.listen({ port: 3000 });

Bun

import { Elysia } from "elysia";

const app = new Elysia();
app.get("/", () => "Hello World!");
app.listen(3000);

The following is the result of running a 1M request load for 50, 100, and 300 concurrent connections.

Great! So Bun is about two times faster than Node.js. For a simple hello world case, Bun is able to reach 150K RPS, while Node.js reaches around 83K. Two times faster is still a big achievement.

Next, let’s increase the complexity by adding HTTPS.

HTTPS hello world

Node.js

import Fastify from "fastify";
import { readFileSync } from "node:fs";

const fastify = Fastify({
logger: false,
https: {
key: readFileSync("/Users/mayankc/Work/source/certs/key.pem"),
cert: readFileSync("/Users/mayankc/Work/source/certs/cert.pem"),
},
});

fastify.get("/", (_, reply) => {
reply.send("Hello world!");
});

fastify.listen({ port: 3000 });

Bun

import { Elysia } from "elysia";

const app = new Elysia();
app.get("/", () => "Hello World!");
app.listen({
port: 3000,
certFile: "/Users/mayankc/Work/source/certs/cert.pem",
keyFile: "/Users/mayankc/Work/source/certs/key.pem",
});

The following is the result of running a 1M request load for 50, 100, and 300 concurrent connections.

With HTTPS enabled, Bun’s RPS goes from 160K to 130K, while Node’s RPS goes from 80K to 64K. Bun is still two times faster than Node.js.

The third and the final prep case is to process JSON received in the request body over HTTS.

HTTPS JSON

In our pursuit to assess various frameworks, the final benchmark entails processing a JSON request via an HTTPS connection. Given our specific use case, which involves a QR code generator API, we will create a simulated handler for this evaluation. This handler will be responsible for extracting the “urlToEmbed” parameter from the request body and subsequently returning it within the response body.

Node.js

import Fastify from "fastify";
import { readFileSync } from "node:fs";

const fastify = Fastify({
logger: false,
https: {
key: readFileSync("/Users/mayankc/Work/source/certs/key.pem"),
cert: readFileSync("/Users/mayankc/Work/source/certs/cert.pem"),
},
});

const reqHandler = (request, reply) => {
if (request.body && request.body.urlToEmbed) {
reply.send({ urlToEmbed: request.body.urlToEmbed });
return;
}

reply.code(400);
reply.send();
};

fastify.post("/", reqHandler);

fastify.listen({ port: 3000 });

Bun

import { Elysia } from "elysia";

const app = new Elysia();

const reqHandler = (ctx) => {
if (ctx.body && ctx.body.urlToEmbed) {
return { urlToEmbed: ctx.body.urlToEmbed };
}

return ctx.set.status = 400;
};
app.post("/", reqHandler);
app.listen({
port: 3000,
certFile: "/Users/mayankc/Work/source/certs/cert.pem",
keyFile: "/Users/mayankc/Work/source/certs/key.pem",
});

The following is the result of running a 1M request load for 50, 100, and 300 concurrent connections.

In the concluding assessment of the framework performance (referred to as the “dummy test” for simplicity), Bun maintains a significant lead, boasting a two-fold speed advantage over Node.js. What adds intrigue to this observation is the notable decrease in Requests Per Second (RPS) figures. Across the first to the third use case, RPS experienced a dramatic 100% decline. For Bun, this translated to a decrease from 160,000 to 80,000 RPS, while Node.js similarly dropped from 82,000 to 45,000 RPS. It’s worth acknowledging that Bun’s sustained 80,000 RPS performance is indeed commendable.

To recap this segment on the “dummy framework test,” Bun proves itself to be not five, but two times faster than Node.js, which is still a remarkable feat. Now, let’s transition our focus to the QR generator API, where the dynamics might take an interesting turn.

QR generator API

In the context of evaluating the QR generator API, our approach within the application involves the following key steps:

1. Secure Communication: The application operates exclusively over HTTPS, ensuring the confidentiality and integrity of data exchange.

2. Parameter Extraction: The application extracts the essential “urlToEmbed” parameter from the incoming request body. This parameter serves as the input to generate the QR code.

3. QR Code Generation: To create the QR code, the application leverages the qrcode module from NPM. This module facilitates the efficient generation of a QR code representation.

4. HTTP Response: Upon successful generation, the application promptly returns the resulting QR code in PNG format via the HTTP response. This allows seamless integration and retrieval of the QR code by the requesting client or application.

The application code is very simple:

Node.js

import Fastify from "fastify";
import * as qrcode from "qrcode";
import { readFileSync } from "node:fs";

async function requestHandler(request, reply) {
if (!(request.body && request.body.urlToEmbed)) {
reply.code(400);
reply.send({ message: "urlToEmbed parameter is missing" });
return;
}

const qrCode = await qrcode.toBuffer(request.body.urlToEmbed, { width: 512 });
reply.code(200);
reply.type("image/png");
reply.send(qrCode);
}

const app = Fastify({
logger: false,
https: {
key: readFileSync("/Users/mayankc/Work/source/certs/key.pem"),
cert: readFileSync("/Users/mayankc/Work/source/certs/cert.pem"),
},
});
app.post("/qr", requestHandler);
app.listen({ port: 3000 });

Bun

import { Elysia } from "elysia";
import * as qrcode from "qrcode";

const reqHandler = async (ctx) => {
if (!(ctx.body && ctx.body.urlToEmbed)) {
ctx.set.status = 400;
return;
}

const qrCode = await qrcode.toBuffer(ctx.body.urlToEmbed, {
width: 512,
});
ctx.set.headers = {
"content-type": "image/png",
};
return new Response(qrCode.buffer);
};

const app = new Elysia();
app.post("/qr", reqHandler);
app.listen({
port: 3000,
certFile: "/Users/mayankc/Work/source/certs/cert.pem",
keyFile: "/Users/mayankc/Work/source/certs/key.pem",
});

The Node.js and Bun code exhibits remarkable similarity. Let’s proceed to execute the test and examine the outcomes.

It’s important to mention that we have opted not to utilize cluster mode in our performance benchmarking, primarily due to the current lack of support for this feature in Bun. Regardless, it’s worth noting that cluster mode typically amplifies the metrics provided by a single process, leading to enhanced performance outcomes.

Results

Executing each test involves processing 10,000 requests with varying levels of concurrency, specifically 5, 10, and 25 concurrent connections. You might wonder why we’ve opted for relatively low total request and concurrency numbers. The reason is straightforward: QR code generation is a resource-intensive task. As we delve into the outcomes, you’ll gain a clear understanding of why these modest figures were chosen.

Now, let’s delve into the results:

Analysis

Let’s revisit the crucial question: Is Bun significantly faster than Node.js? The answer is a bit nuanced. In simpler, less resource-intensive scenarios, Bun does exhibit superior performance compared to Node.js. However, the story takes an unexpected turn when we put both technologies to the test by deploying our QR generator application. To our astonishment, Bun’s performance lags behind that of Node.js. It’s not 5 times, 2 times, 50%, or even 10% faster than Node.js; instead, it is marginally slower. This outcome was completely contrary to our initial expectations.

Furthermore, it’s worth noting that the Requests Per Second (RPS) metric for the QR generator API is a mere 50. Yes, you read that correctly — not 50,000, just 50. It’s akin to a significant drop-off from a lofty mountain peak. Bun, which initially boasted 160K RPS, now stands at 50. Similarly, Node.js, with its original 80K RPS, also clocks in at 50. The real-world performance challenges can be quite harsh, indeed.

There might be arguments like:

  • Test system being MacBook: It’s important to acknowledge that our test system is a MacBook, which may not be classified as server-grade hardware. However, it’s worth mentioning that Node.js is also running on the same MacBook, ensuring a level playing field for our evaluations.
  • Intensive work: QR code generation is a computationally intensive task, and this is equally true for Node.js. It’s worth noting that Bun may demonstrate superior performance in scenarios involving more asynchronous operations, such as JWT verification and database reads. However, these specific scenarios are better suited for discussion in a separate article.
  • QR code package: While the QR code package we’re using is designed for Node.js, it’s important to highlight that it’s the most popular choice available on the Node Package Manager (NPM).

Thanks for reading!

We’d love to hear your opinions on these observations.

--

--