Bun vs Java: Native HTTP server performance comparison for hello world case

Mayank C
Tech Tonic

--

The recent articles on the performance comparison series have gotten quite popular in the medium community. Initially, I did a comparison of Node/Deno/Bun’s native server with SpringBoot. I got comments that this was an unfair comparison. SpringBoot is a framework and must be compared with a framework on the other side. Honoring the comments, I did a follow-up comparison of SpringBoot with Node/Deno/Bun running on express/fastify. I still got comments that a comparison of native HTTP servers on both sides would be very useful.

In this series, I’m going to compare native servers provided by Node.js, Deno, Bun, and Go with Java. The other articles are:

This article compares Bun’s native server with Java’s native server. So far, I’ve seen that Bun’s native server is the fastest of Node.js, Deno, and Bun. The results are going to be predictable.

All the recent articles can be seen on my author page.

The setup

The tests are executed on MacBook Pro M1 with 16G of RAM.

The software versions are:

  • Bun v0.5.8
  • Java 20

The native hello world HTTP server code in both cases is as follows:

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 });
}
},
});

Java

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class NativeHelloWorldServer {

public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
server.createContext("/", new MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}

static class MyHandler implements HttpHandler {
@Override
public void handle(HttpExchange t) throws IOException {
String response = "Hello world!";
t.sendResponseHeaders(200, response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
}

Execution

Each test is executed for 5M (5 million) requests.

The tests are executed for 25, 100, and 300 concurrent connections.

The load test is carried out using Bombardier HTTP testing tool.

The following are the tables showing results for each concurrency level:

Well, Bun’s native HTTP server turns out to be way (way) faster than Java’s native server. The difference is pretty significant whether it’s RPS, latency or system resources. Also, Bun uses very less system resources like CPU and memory to offer better RPS. Especially, Bun’s memory usage (~30M) is nothing when compared to Java’s memory usage (~400M).

I believe the primary reason is that the server isn’t doing much. It is mostly I/O bound. When things get complex, I guess Java should outperform Node.js, Deno, and Bun.

WINNER: Bun

There might an argument about the usage of cached thread pool in Java. The other option is to allocate a fixed-size thread pool at the start. Here is a quick test using 100 concurrent connections with a fixed-size thread pool of 125 threads. The only line change in Java code is:

//server.setExecutor(Executors.newCachedThreadPool());
server.setExecutor(Executors.newFixedThreadPool(125));

The results with fixed-size thread pool are as follows:

I don’t see any significant performance improvements with a fixed-size thread pool.

In the following article, the comparison with Java will be presented for Go.

The other articles that are available:

Thanks for reading. All comments, suggestions are welcome (especially on the Java part).

--

--