Rust vs. Go: The benchmark
Tags: webdev, programming, rust, go
Published: 2024-01-17 02:58:44

## 👋  Introduction

Lately, there's been a lot of buzz around Rust & Go. Why all the hype? Which one truly holds the edge? Let's fire up servers on 127.0.0.1 & find out.

> #### GitHub: Here's the Simple HTTP Server code for benchmarking:
- **Web Server Code**: Access the server implementations for both languages:
  - Rust: [Rust-Serve Code](https://github.com/ZanePearton/Rust-serve)
  - Go: [Go-Serve Code](https://github.com/ZanePearton/Go-Serve)

## 📦 Benchmarking Setup with WRK
To start, we'll need WRK(powerful benchmarking tool). If you're on a Mac like me - you'll find it on HomeBrew.
- **Benchmarking Tool**:
We're using **WRK**
- **Installation**: 
Mac users, you can easily install **WRK** with Homebrew command:
  ```
  brew install wrk
  ```


## 🔧 Setting Up the Rust Web Server
For Rust, you'll need a specific directory structure to get started:
```
|_ Cargo.toml
|_ src
   |_ main.rs
```
Once everything is in place, initiating the server is straightforward with **cargo**:
```
cargo build --release
cargo run
```
![Terminal GH GIF](https://raw.githubusercontent.com/ZanePearton/Rust-serve/main/build.gif)

## 🏗 Rust Web Server Code
Interested in the nitty-gritty? Dive into the server code!
#### Explore the Code

> Rust: **main.rs**

```
// Import necessary modules and types from the actix_web crate and standard library.
use actix_web::{web, App, HttpServer, HttpResponse, Responder, middleware::Logger};
use std::env;

// The main function is marked with `actix_web::main`, which sets up an async runtime.
// This function will return a Result that, if an error occurs, will contain an `std::io::Error`.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Set the environment variable for logging level to "info" for actix_web logs.
    env::set_var("RUST_LOG", "actix_web=info");
    // Initialize the env_logger logger, which will log information based on the RUST_LOG environment variable.
    env_logger::init();

    // Create and run an HTTP server.
    HttpServer::new(|| {
        // Initialize the Actix web application.
        App::new()
            // Add the Logger middleware to log all incoming requests.
            .wrap(Logger::default())
            // Define a route for the root URL ("/") that handles GET requests with the `root_handler` function.
            .route("/", web::get().to(root_handler))
    })
    // Bind the server to listen on the localhost address and port 8080.
    .bind("127.0.0.1:8080")?
    // Start the server and await its completion, handling any errors that occur.
    .run()
    .await
}

// Define an asynchronous handler function for the root URL.
// This function returns a type that implements the `Responder` trait, which can be converted into an HTTP response.
async fn root_handler() -> impl Responder {
    // Create an HTTP response with the status code 200 OK and the body "Hello, World! This is a cool web test!".
    HttpResponse::Ok().body("Hello, World! This is a cool web test!")
}

```
> Rust: **Cargo.toml**
```
[package]
name = "rust_server"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.0"
actix-rt = "2.5"
env_logger = "0.9"

[dev-dependencies]
criterion = "0.3"

```
## 🔧 Setting Up the Go Web Server
For Go, Effortlessly run the following:
```
go run main.go
```
## 🏗 Go Web-Server Code
Interested in the nitty-gritty? Dive into the server code!
#### Server Code

> Go: **main.go**

```
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func main() {
	// Create a new ServeMux (router)
	mux := http.NewServeMux()

	// Register a handler function for the root URL
	mux.HandleFunc("/", rootHandler)

	// Create a new HTTP server
	server := &http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: logRequest(mux), 
                // Wrap the handler with the logging middleware
	}

	// Start the server and log if there's an error
	fmt.Println("Server is running on http://127.0.0.1:8090")
	if err := server.ListenAndServe(); err != nil {
		log.Fatal("Error starting server: ", err)
	}
}

// rootHandler responds to requests at the root URL
func rootHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello, World! This is a cool web test!")
}

// logRequest is a middleware that logs each request
func logRequest(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		duration := time.Since(start)
		log.Printf("Request: %s %s, Duration: %s\n", r.Method, r.URL.Path, duration)
	})
}

```
### 🔑 Rust or Go: Key Metrics

> Rust: Metrics Release Mode Not Enabled

```
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080  
Running 30s test @ http://127.0.0.1:8080
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     7.36ms    5.32ms  68.59ms   91.98%
    Req/Sec     7.54k     1.51k   12.48k    69.83%
  Latency Distribution
     50%    5.94ms
     75%    7.67ms
     90%   10.77ms
     99%   33.52ms
  450491 requests in 30.02s, 48.98MB read
Requests/sec:  15004.41
Transfer/sec:      1.63MB

```
> Rust: Metrics Release - Enabled

```
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080                                                      
Running 30s test @ http://127.0.0.1:8080
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.41ms    6.39ms  95.91ms   92.02%
    Req/Sec     9.47k     2.42k   14.95k    65.50%
  Latency Distribution
     50%    4.73ms
     75%    6.31ms
     90%   10.66ms
     99%   36.55ms
  566141 requests in 30.05s, 61.55MB read
Requests/sec:  18838.67
Transfer/sec:      2.05MB

```

> Go: Metrics

```
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8090                                                      
Running 30s test @ http://127.0.0.1:8090
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.12ms    3.13ms  46.54ms   94.85%
    Req/Sec    18.12k     3.18k   25.08k    69.95%
  Latency Distribution
     50%    2.62ms
     75%    3.23ms
     90%    4.23ms
     99%   20.40ms
  1082616 requests in 30.05s, 160.03MB read
Requests/sec:  36021.81
Transfer/sec:      5.32MB
```
### 🔍 Lets Chart it.. Rust or Go?:
Let's compare performance of Rust server & Go servers:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/owzvaol6p9cl2s2euh26.png)




### 📈 Rust Server Performance: Release Mode - Not Enabled
> - **Average Latency**: 7.36ms
- **Standard Deviation of Latency**: 5.32ms
- **Maximum Latency**: 68.59ms
- **Requests per Second**: 7.54k (average), 12.48k (maximum)
- **Latency Distribution**:
  - Median (50%): 5.94ms
  - 75th Percentile: 7.67ms
  - 90th Percentile: 10.77ms
  - 99th Percentile: 33.52ms
- **Total Requests**: 450,491 in 30.02 seconds
- **Data Transferred per Second**: 1.63MB

### 📈 Rust Server Performance: Release Mode - Enabled
> - **Average Latency**: 6.41ms (down from 7.36ms)
- **Standard Deviation of Latency**: 6.39ms (up from 5.32ms)
- **Maximum Latency**: 95.91ms (up from 68.59ms)
- **Requests per Second**: 
  - Average: 9.47k (up from 7.54k)
  - Maximum: 14.95k (up from 12.48k)
- **Latency Distribution**:
  - Median (50%): 4.73ms (down from 5.94ms)
  - 75th Percentile: 6.31ms (down from 7.67ms)
  - 90th Percentile: 10.66ms (up from 10.77ms)
  - 99th Percentile: 36.55ms (up from 33.52ms)
- **Total Requests**: 566,141 in 30.05 seconds (up from 450,491)
- **Data Transferred per Second**: 2.05MB (up from 1.63MB)

### 📈 Go Server Performance:
> - **Average Latency**: 3.85ms
- **Standard Deviation of Latency**: 3.72ms
- **Maximum Latency**: 60.31ms
- **Requests per Second**: 14.71k (average), 24.66k (maximum)
- **Latency Distribution**:
  - Median (50%): 3.14ms
  - 75th Percentile: 3.95ms
  - 90th Percentile: 5.45ms
  - 99th Percentile: 21.33ms
- **Total Requests**: 879,736 in 30.06 seconds
- **Data Transferred per Second**: 4.33MB

### 🤔 Well.. Is it Rust or is it Go?

- **Latency**: The Go server has lower average, median, and 99th percentile latencies compared to the Rust server. This suggests that for each individual request, the Go server is generally able to respond faster.
  
- **Requests per Second (Throughput)**: The Go server is handling nearly double the number of requests per second compared to the Rust server. This indicates that the Go server has a higher throughput under the tested load conditions.
  
- **Data Transfer**: The Go server is transferring more data per second than the Rust server, which aligns with its higher requests per second.

### 😎 Conclusion:

Well.. In this benchmark, the Go server is outperforming the Rust server in terms of both latency and throughput. However, it's important to note a few key considerations when interpreting these results:

- **Server Configuration**: The configuration of the server, such as the use of asynchronous code, thread pool sizes, and other optimizations, can significantly impact performance.
  
- **Workload Characteristics**: Depending on what the servers are actually doing (static file serving, database queries, CPU-bound tasks), the performance characteristics could change.
  
- **Benchmarking Conditions**: The system on which the benchmark is run, other running processes, network conditions, and even the specifics of how `wrk` is used can affect the results.

- **Code Maturity and Optimizations**: The specific Rust and Go code being benchmarked could be at different levels of optimization. More mature or optimized code can perform significantly better.