Java vs Rust: Hello world HTTP server performance comparison

Mayank C
Tech Tonic

--

This is an OUTDATED article. A better & practical comparison is available here.

In this article, I’m going to compare Java’s native HTTP server with Rust’s Hyper HTTP server. Based on my research, I’ve found that Hyper is the most popular server choice on the Rust side. If there are better and popular alternatives, please let me know.

Rust is a compiled language that produces machine code, while Java is also a compiled that produces a bytes code that runs inside JVM. The comparison may not be that fair. Anyway, let’s run the tests and check the results.

Before moving ahead, other comparisons are here:

Test setup

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

The software versions are:

  • Java v20
  • Rust v1.68.2

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

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

}

Rust

use std::convert::Infallible;
use std::net::SocketAddr;

use bytes::Bytes;
use http_body_util::Full;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use tokio::net::TcpListener;

async fn hello(_: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let response = Response::builder()
.header("Content-type", "text/plain")
.body(Full::new(Bytes::from("Hello World!")))
.unwrap();
Ok(response)
}

#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr: SocketAddr = ([127, 0, 0, 1], 3000).into();
let listener = TcpListener::bind(addr).await?;
loop {
let (stream, _) = listener.accept().await?;
tokio::task::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(stream, service_fn(hello)).await {
println!("Error serving connection: {:?}", err);
}
});
}
}

Cargo.toml

[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

[dependencies]
bytes = "1"
hyper = { version = "1.0.0-rc.3", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1.0-rc.2"

Rust code has been built in release mode:

> cargo build --release
...
Compiling hyper v1.0.0-rc.3
Compiling hello_world v0.1.0 (/Users/mayankc/Work/source/perfComparisons/rust)
Finished release [optimized] target(s) in 19.88s

Execution

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

The tests are executed for 10, 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:

Analysis

Rust totally beats Java on all aspects whether it’s RPS, latencies, CPU or memory.

WINNER: Rust

--

--