Rust Axum vs. Go Fiber: HTTP/2 Multiplexing and Peak Connection Benchmarking
HTTP/2 Multiplexing: A Foundational Performance Differentiator
When evaluating high-performance web frameworks, understanding their underlying network protocol capabilities is paramount. HTTP/2, with its multiplexing, header compression, and server push features, offers significant advantages over HTTP/1.1. For frameworks like Rust’s Axum and Go’s Fiber, the implementation and efficiency of HTTP/2 support, particularly multiplexing, directly impact their ability to handle concurrent requests and achieve peak connection throughput. Multiplexing allows multiple requests and responses to be interleaved over a single TCP connection, drastically reducing latency and improving resource utilization.
Benchmarking Methodology: Tools and Setup
To rigorously compare Axum and Fiber, we’ll employ a standardized benchmarking approach. This involves setting up identical test environments and utilizing robust load testing tools. Our primary tool will be wrk, a modern HTTP benchmarking tool capable of generating high concurrency and supporting HTTP/2. We will also use ab (ApacheBench) for baseline comparisons and k6 for more advanced scenario scripting.
The test environment will consist of:
- A dedicated, isolated server instance (e.g., AWS EC2 m5.large or equivalent) with sufficient CPU and RAM.
- A clean operating system installation (e.g., Ubuntu 22.04 LTS).
- The benchmarking client (
wrk,k6) running on a separate, powerful machine to avoid client-side bottlenecks. - Both Axum and Fiber applications compiled in release mode.
- TLS enabled for both frameworks to simulate realistic production traffic, as HTTP/2 is typically negotiated over TLS (h2 or h2c for cleartext, though less common in production).
Axum Implementation: HTTP/2 with Tokio and Hyper
Axum, built on top of Tokio and Hyper, inherits robust HTTP/2 support. Hyper is a low-level HTTP implementation that provides excellent performance and flexibility. For Axum to serve HTTP/2, we need to configure the underlying Tokio runtime and Hyper server to use TLS and enable HTTP/2. This typically involves generating or obtaining SSL certificates and configuring the listener.
Axum Server Setup (HTTP/2 with TLS)
Here’s a minimal Axum application demonstrating HTTP/2 setup. We’ll use self-signed certificates for this example. In production, you would use certificates from a trusted Certificate Authority.
Generating Self-Signed Certificates
First, generate a self-signed certificate and private key:
openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'
Axum Application Code
The Rust code will load these certificates and configure the Axum service.
use axum::{routing::get, Router};
use tokio_rustls::{
rustls::{Certificate, PrivateKey, ServerConfig},
TlsAcceptor,
};
use std::sync::Arc;
use std::fs;
use std::io;
use std::net::SocketAddr;
async fn root() -> &'static str {
"Hello, Axum!"
}
#[tokio::main]
async fn main() -> io::Result<()> {
// Load certificate and private key
let cert_file = fs::File::open("cert.pem")?;
let mut reader = io::BufReader::new(cert_file);
let certs = rustls_pemfile::certs(&mut reader)?
.into_iter()
.map(rustls::Certificate)
.collect();
let key_file = fs::File::open("key.pem")?;
let mut reader = io::BufReader::new(key_file);
let keys = rustls_pemfile::pkcs8_private_keys(&mut reader)?
.into_iter()
.map(rustls::PrivateKey)
.collect::>();
if keys.is_empty() {
panic!("No private keys found");
}
let key = keys[0].clone();
// Build the TLS configuration
let tls_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config));
// Build the Axum router
let app = Router::new().route("/", get(root));
// Create a TCP listener
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
// Create the Tokio runtime and run the server
let server_handle = tokio::spawn(async move {
loop {
let (socket, _) = listener.accept().await.unwrap();
let tls_acceptor = tls_acceptor.clone();
let app = app.clone(); // Clone the app for each connection
tokio::spawn(async move {
if let Ok(stream) = tls_acceptor.accept_with(socket).await {
// Hyper server configuration for HTTP/2
let mut builder = hyper::server::conn::Http::new();
builder.http2_only(true); // Explicitly enable HTTP/2
let service = hyper::service::service_fn(move |req| {
// Axum's service implementation
app.call(req)
});
if let Err(err) = builder.serve_connection(stream, service).await {
eprintln!("Error serving connection: {:?}", err);
}
}
});
}
});
server_handle.await?;
Ok(())
}
Note the explicit builder.http2_only(true); which ensures Hyper attempts to negotiate HTTP/2. Tokio’s TLS integration with Hyper handles the ALPN negotiation for HTTP/2.
Fiber Implementation: HTTP/2 with Fasthttp
Go Fiber is a web framework built on top of Fasthttp, known for its exceptional performance. Fasthttp itself is highly optimized for speed and low-level network operations. While Fasthttp primarily focuses on HTTP/1.1 performance, it has experimental support for HTTP/2. Enabling HTTP/2 in Fiber requires careful configuration of the underlying Fasthttp server, particularly regarding TLS and the HTTP/2 protocol.
Fiber Server Setup (HTTP/2 with TLS)
Similar to Axum, we’ll use self-signed certificates. Fiber’s configuration for HTTP/2 is more direct via its server options.
Generating Self-Signed Certificates (Go)
Use the same openssl command as for Axum:
openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'
Fiber Application Code
The Go code will configure Fiber to use TLS and enable HTTP/2.
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
)
func main() {
// Create a new Fiber app
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
// Enable HTTP/2 by setting TLSConfig
// Fasthttp automatically enables HTTP/2 when TLSConfig is provided
// and the client negotiates it.
TLSConfig: &tls.Config{
Certificates: []*tls.Certificate{
// Load certificate and key
&tls.Certificate{
Certificate: [][]byte{certPEM}, // Replace with actual cert PEM bytes
PrivateKey: privateKeyPEM, // Replace with actual private key PEM bytes
},
},
// Optional: Set NextProtos for explicit ALPN negotiation
NextProtos: []string{"h2", "http/1.1"},
},
})
// Middleware
app.Use(logger.New())
// Routes
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, Fiber!")
})
// Start server on port 3000 with TLS
log.Fatal(app.ListenTLS(":3000", "cert.pem", "key.pem"))
}
// Placeholder for certificate and key bytes. In a real app, load from files.
var certPEM []byte
var privateKeyPEM []byte
func init() {
// Load certificate and private key from files
certPEMBytes, err := ioutil.ReadFile("cert.pem")
if err != nil {
log.Fatalf("Failed to read certificate file: %v", err)
}
privateKeyPEMBytes, err := ioutil.ReadFile("key.pem")
if err != nil {
log.Fatalf("Failed to read private key file: %v", err)
}
certPEM = certPEMBytes
privateKeyPEM = privateKeyPEMBytes
}
Fiber’s ListenTLS function, when provided with certificate and key files, implicitly configures Fasthttp to support HTTP/2 if the client negotiates it via ALPN. The NextProtos field in tls.Config is crucial for explicit ALPN negotiation.
Benchmarking with wrk
We will use wrk to simulate concurrent users and measure the throughput and latency of both applications. The key is to enable HTTP/2 support in wrk.
wrk Setup for HTTP/2
Ensure you have a recent version of wrk compiled with HTTP/2 support. You can typically enable this during compilation or by using a pre-built binary that includes it. The command-line flag --http2 is used.
Benchmarking Axum
Run the Axum server, then execute the wrk command:
# On the server where Axum is running cargo run --release # On the client machine wrk -t8 -c1000 --http2 -d30s --latency https://your_server_ip:3000/
Explanation of flags:
-t8: Use 8 threads.-c1000: Maintain 1000 concurrent connections.--http2: Enable HTTP/2 protocol.-d30s: Run the benchmark for 30 seconds.--latency: Record latency statistics.https://your_server_ip:3000/: The target URL.
Benchmarking Fiber
Run the Fiber server, then execute the wrk command:
# On the server where Fiber is running go run main.go # On the client machine wrk -t8 -c1000 --http2 -d30s --latency https://your_server_ip:3000/
The wrk command remains identical, allowing for a direct comparison.
Interpreting Results: Multiplexing and Throughput
When analyzing the wrk output, pay close attention to:
- Requests/sec: Higher is better, indicating more requests processed.
- Latency (Avg, Min, Max, 99%): Lower latency is crucial for user experience. HTTP/2’s multiplexing should ideally reduce tail latencies under high load.
- Transfer/sec: Indicates data throughput.
- Socket errors: Any errors suggest connection instability or resource exhaustion.
Expected Observations Regarding HTTP/2 Multiplexing:
- Under high concurrency (e.g., 1000+ connections), both frameworks should demonstrate significantly better performance with HTTP/2 compared to HTTP/1.1. This is due to multiplexing reducing the overhead of establishing multiple TCP connections and the head-of-line blocking inherent in HTTP/1.1.
- The framework that more efficiently manages its internal event loops and I/O operations will likely show higher requests/sec and lower latency. Rust’s asynchronous model with Tokio and Hyper is generally very performant, while Go’s goroutines and efficient scheduler also provide excellent concurrency.
- Differences might emerge in how gracefully each framework handles connection churn, error conditions, and resource contention at extreme loads.
Advanced Benchmarking: Connection Limits and Resource Saturation
To truly stress-test the HTTP/2 implementation and multiplexing capabilities, we need to push beyond typical load. This involves increasing the number of concurrent connections significantly and observing how each framework behaves as it approaches its resource limits (CPU, memory, file descriptors).
Pushing Connection Limits with wrk
Gradually increase the -c (connections) parameter in wrk, starting from 1000 and incrementing by 500 or 1000. Monitor server-side resource utilization (CPU, memory, network I/O, open file descriptors) using tools like htop, vmstat, and lsof.
# Example: Pushing connections for Axum wrk -t16 -c5000 --http2 -d60s --latency https://your_server_ip:3000/ # Example: Pushing connections for Fiber wrk -t16 -c5000 --http2 -d60s --latency https://your_server_ip:3000/
Server-Side Monitoring and Tuning
On the server, use the following to monitor:
# Monitor CPU and Memory htop # Monitor network and I/O vmstat 1 # Monitor open file descriptors sudo lsof -p <PID_of_your_app> | wc -l sudo sysctl fs.file-max sudo sysctl fs.nr_open
If you encounter Too many open files errors, you may need to increase the system’s file descriptor limits. This is a common bottleneck for high-concurrency servers.
# Temporarily increase limits (for current session) ulimit -n 65536 # Permanently increase limits (edit /etc/security/limits.conf) # * soft nofile 65536 # * hard nofile 65536 # Apply system-wide limits (edit /etc/sysctl.conf) # fs.file-max = 2097152 # fs.nr_open = 1048576
Performance Analysis: Axum vs. Fiber HTTP/2
The results from these benchmarks will highlight key differences:
- Raw Throughput (Requests/sec): Which framework sustains higher request rates at peak load? This often comes down to the efficiency of the underlying HTTP implementation and the runtime’s concurrency model.
- Latency Under Load: How does latency (especially tail latency) degrade as connection counts increase? Effective multiplexing should minimize this degradation.
- Resource Utilization (CPU/Memory): Which framework is more memory-efficient per connection? Which utilizes CPU more effectively?
- Stability and Error Handling: At extreme loads, which framework is more resilient to errors and connection failures?
Rust Axum (Tokio/Hyper): Generally offers very fine-grained control and predictable performance due to its explicit async model and mature ecosystem. Hyper is a highly optimized HTTP library. Performance can be exceptional, but requires careful management of async tasks and resources.
Go Fiber (Fasthttp): Leverages Go’s efficient goroutine scheduler and Fasthttp’s low-level optimizations. Often simpler to achieve high concurrency with less explicit code. Performance is typically excellent, though Fasthttp’s HTTP/2 support might be less mature or performant than Hyper’s in certain edge cases.
Conclusion: Choosing the Right Tool for HTTP/2 Demands
Both Axum and Fiber are capable of high-performance HTTP/2 serving. The choice between them for demanding, high-concurrency applications hinges on specific workload characteristics, team expertise, and the nuances revealed by rigorous benchmarking. Axum, with its foundation in Tokio and Hyper, offers a robust and highly tunable HTTP/2 implementation. Fiber, leveraging Fasthttp and Go’s concurrency primitives, provides a compelling, often simpler, path to high throughput. For scenarios where absolute maximum performance and fine-grained control over network I/O are critical, Axum might have an edge. For rapid development and excellent out-of-the-box performance, Fiber is a strong contender. Always validate with your specific use case and load patterns.