Skip to content

HTTP/2 slower than HTTP/1.1 #3841

@ThomasVille

Description

@ThomasVille

Expected Behavior

HTTP/2 performance should be at least comparable to HTTP/1.1, if not better due to protocol features like multiplexing and header compression.

Current Behavior

HTTP/2 is substantially slower than HTTP/1.1 when using actix-web with rustls. Furthermore, a comparable Node.js http2 server significantly outperforms the actix-web HTTP/2 implementation. Server logs show that endpoint processing times are nearly identical across all implementations, suggesting the performance difference lies in the HTTP/2 protocol handling layer rather than application logic.

I'm using reqwest with custom certificates so this might have an effect on the result, but the same client code doesn't show any large performance drop when testing against a NodeJS server.

Performance Results (1000 requests)

Average time per request in milliseconds using the reqwest client in the reproduction repo.

Protocol Node.js Actix-web
HTTP/1.1 0.32ms 0.14ms
HTTP/2 0.58ms 41.45ms

Possible Solution

Potential areas to investigate:

  1. Configuration parameters for HTTP/2 in actix-web that might need tuning
  2. Interaction between actix-web and reqwest specifically
  3. Buffer sizes, connection pooling, or other transport-level settings
  4. Potential optimizations in the listen_rustls_0_23 binding
  5. Comparison with other TLS backends (e.g., native-tls, openssl)

Steps to Reproduce

  1. Clone the reproduction repository: https://github.com/ThomasVille/reqwest-actix-web-slow-http2.
  2. Generate SSL certificates using the provided script: ./generate_certs.sh
  3. Start the actix-web server with HTTP/2:
    cd actix-web-server
    cargo run --release
  4. Run benchmarks using the reqwest client:
    cd ../reqwest_client
    cargo run --release
  5. Compare with HTTP/1.1 by stopping the server and restarting with:
    cd actix-web-server
    USE_HTTPS=false cargo run --release
  6. Run HTTP/1.1 benchmarks:
    cd ../reqwest_client
    USE_HTTPS=false cargo run --release

Reproduction Code

Here is the entire repo: https://github.com/ThomasVille/reqwest-actix-web-slow-http2.

And here is some sample code in case you don't want to go through the repo:

actix-web server (Rust)
use actix_web::{get, App, HttpRequest, HttpResponse, HttpServer};
use rustls::ServerConfig;
use std::fs::File;
use std::io::BufReader;

fn server_config() -> ServerConfig {
    rustls::crypto::ring::default_provider().install_default().ok();

    let cert_file = File::open("./cert.pem").expect("cert file");
    let key_file = File::open("./key.pem").expect("key file");

    let certs = rustls_pemfile::certs(&mut BufReader::new(cert_file))
        .collect::<Result<Vec<_>, _>>()
        .expect("certs");

    let key = rustls_pemfile::pkcs8_private_keys(&mut BufReader::new(key_file))
        .next()
        .expect("key")
        .expect("key parse");

    ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, rustls::pki_types::PrivateKeyDer::Pkcs8(key))
        .expect("config")
}

#[get("/data")]
async fn get_random_data() -> HttpResponse {
    // Generate 1-8KB of random JSON data
    let data = generate_random_content(); // implementation details omitted
    HttpResponse::Ok().json(data)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let server = HttpServer::new(|| App::new().service(get_random_data));

    // For HTTP/2
    let listener = std::net::TcpListener::bind("127.0.0.1:8443")?;
    server.listen_rustls_0_23(listener, server_config())?.run().await

    // For HTTP/1.1 comparison
    // server.bind("127.0.0.1:8080")?.run().await
}
Node.js http2 server (for comparison)
const http2 = require('http2');
const fs = require('fs');

function handleRequest(req, res) {
    if (req.url === '/data') {
        const content = generateRandomContent(); // identical to Rust version
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(content));
    }
}

const options = {
    key: fs.readFileSync('./key.pem'),
    cert: fs.readFileSync('./cert.pem'),
    allowHTTP1: true
};

const server = http2.createSecureServer(options, handleRequest);
server.listen(8443, '127.0.0.1');
reqwest client (Rust) - for testing
use reqwest::Client;
use std::time::Instant;

fn build_client() -> Client {
    reqwest::ClientBuilder::new()
        .use_rustls_tls()
        .add_root_certificate(
            reqwest::Certificate::from_pem(include_bytes!("./rootCA.pem"))
                .expect("Failed to load certificate"),
        )
        .build()
        .expect("Failed to build client")
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = build_client();

    let start = Instant::now();
    let mut total_requests = 0;

    for _ in 0..1000 {
        let response = client
            .get("https://127.0.0.1:8443/data")
            .send()
            .await?;
        let _body = response.text().await?;
        total_requests += 1;
    }

    let duration = start.elapsed();
    println!("Total requests: {}", total_requests);
    println!("Total time: {:?}", duration);
    println!("Avg per request: {:?}", duration / total_requests);

    Ok(())
}

Cargo.toml:

[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
    "http2",
    "json",
    "rustls-tls",
    "rustls-tls-native-roots"
] }
tokio = { version = "1", features = ["full"] }

Your Environment

  • Rust Version (output of rustc -V): 1.89.0
  • Actix Web Version: 4.9.0
  • rustls version: 0.23 with ring crypto provider
  • actix-rt: 2
  • Operating System: Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions