Bun vs Rust: Hello world performance

Mayank Choubey
Tech Tonic

--

Introduction

The battle of JavaScript/TypeScript runtimes got hotter since the sudden arrival of Bun, a fast all-in-one JavaScript runtime. There was the ruler, Node.js. Then Deno came about two years back. And now, bun, has added itself to the list of JS runtimes.

The king of JavaScript runtimes is undoubtedly Node.js. Node.js has been around for more than 12 years, has been battle tested, is production ready, robust, reliable, etc. Node.js is used both by tech giants and tech startups.

Deno was launched in 2020 by the same Ryan Dahl, to fix the design issues / limitations with Node.js. Deno is a simple, modern and secure runtime for JavaScript, TypeScript, and WebAssembly that uses V8 and is built in Rust. Deno is sandboxed, and is secure by default.

Then comes Bun on July 5th 2022. Bun was ideated and built by Jarred Sumner. Along with Bun, there was an announcement of a company called oven.sh that’ll oversee Bun’s development.

Bun is a modern JavaScript runtime like Node or Deno. It was built from scratch to focus on three main things:

  • Start fast (it has the edge in mind)
  • New levels of performance (extending JavaScriptCore, the engine)
  • Being a great and complete tool (bundler, transpiler, package manager)

Bun implements Node.js’ module resolution algorithm, so you can use npm packages in Bun. ESM and CommonJS are supported, but Bun internally uses ESM.

Bun is designed as a drop-in replacement for your current JavaScript & TypeScript apps or scripts — on your local computer, server or on the edge. Bun natively implements hundreds of Node.js and Web APIs, including ~90% of Node-API functions (native modules), fs, path, Buffer and more. This means that Bun can be used in place of Node.js.

Bun claims that it is about three to four times faster than both Node.js and Deno. A simple application like hello world runs four times faster with Bun. The same extends to npm install, running tests, etc. Bun is all about speed! This is going to be the primary USP for Bun. Run the exact same application, but achieve greater performance!

Over the last few years, Rust has gained tremendous popularity. The tech giants are now using Rust for their system development work. In their own words: Rust is a language empowering everyone to build reliable and efficient software.

The advantages of Rust are:

  • Rust is blazingly fast and memory-efficient: with no runtime or garbage collector, it can power performance-critical services, run on embedded devices, and easily integrate with other languages.
  • Rust’s rich type system and ownership model guarantee memory-safety and thread-safety — enabling you to eliminate many classes of bugs at compile-time.
  • Rust has great documentation, a friendly compiler with useful error messages, and top-notch tooling — an integrated package manager and build tool, smart multi-editor support with auto-completion and type inspections, an auto-formatter, and more.

In this article, we’ll compare Bun and Rust for a simple hello world use case. Rust, being compiled, is expected to be way better than Bun, being interpreted. Let’s find it out.

Test setup

Environment

The tests are executed on the localhost. Both test runner and SUT are local. The test environment is:

  • Mac OS M1 with 16 GB RAM
  • Bun v0.2.0
  • Rust v1.64.0
  • Cargo v1.64.0

Code

Bun

export default {
port: 3000,
fetch(request) {
return new Response("Hello world!");
},
};

Rust

use std::{convert::Infallible, net::SocketAddr};
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
async fn handle(_: Request<Body>) -> Result<Response<Body>, Infallible> {
let response = Response::builder()
.status(200)
.header("Content-type", "text/plain")
.body(Body::from("Hello world!"))
.unwrap();
Ok(response)
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(handle))
});
let server = Server::bind(&addr).serve(make_svc);if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}

The rust code has been compiled in release mode using the following command:

> cargo build --release
...
Compiling rust_hello_world v0.1.0 (/Users/mayankc/Work/source/examples/perfComparisons)
Finished release [optimized] target(s) in 28.75s
> ls -l target/release/rust_hello_world
-rwxr-xr-x 1 mayankc staff 2532960 Sep 27 07:46 target/release/rust_hello_world

Measurements

Metrics

It’s not only about how fast a runtime is. A runtime can be very fast, but may stall the system by using too much CPU, memory, disk, etc. The measurements used by Bun’s official site are mostly requests per second. This doesn’t represent the complete picture. Also, we need to see the response time distribution across different quantiles.

Here is the complete list of measurements:

  • Minimum response time
  • 10th percentile response time
  • 1st quartile response time
  • Mean response time
  • Median response time
  • 3rd quartile response time
  • 90the percentile response time
  • Maximum response time
  • CPU usage
  • Memory usage

Concurrency

The other important thing is concurrency. The number of concurrent connections has a significant impact over response time, and that has an impact over requests per second.

Also, a load test for 1K, 10K or 50K requests is too less to fairly judge a runtime. The test needs to be carried over numerous requests. All the tests done for this article runs to 10M requests for each concurrency level.

A total of 10M requests are executed for the following concurrent connections:

  • 50 concurrent connections
  • 100 concurrent connections
  • 200 concurrent connections

Results

50 concurrent connections

Let’s see how Bun performs in front of Rust for a low concurrency of 50 connections. Here are the charts:

Analysis

Unexpectedly, Bun stood pretty firm in front of a mammoth like Rust. Bun’s hello world took less time to finish off 10M requests. This also means that, Bun’s RPS is more than Rust’s. However, Rust wins most of the metrics till 3rd quartile region. Bun’s CPU usage is about a half of Rust. Memory usage is where Rust wins by a huge landslide. Rust uses about 8-10M of memory, while Bun uses 180M of memory.

100 concurrent connections

Let’s see how Bun performs in front of Rust for a medium concurrency of 100 connections. Here are the charts:

Analysis

At high concurrency, Bun took the lead over Rust by winning in more metrics. Rust’s memory continues to be very, very low when compared to Bun.

200 concurrent connections

Let’s see how Bun performs in front of Rust for a very high concurrency of 200 connections. Here are the charts:

Analysis

The same pattern continues.

Conclusion

Was it fair to compare Bun and Rust? Perhaps not because they work in very different worlds. Bun is more like Deno, Node.js. Deno itself is written in Rust. Just for the sake of comparison, Bun holds up pretty well in front of a giant like Rust. Bun loses to Rust at low & medium concurrency. But at high concurrency, Bun performs better than Rust, though the margin is very little. In terms of memory, Bun is no match for Rust’s extremely low memory usage.

--

--