Hyper + Rustls Extremely Slow?

I have been trying out using Rustles + Hyper, but for some reason, I am getting a long response time for even the smallest requests.

If I send multiple http1.1 requests they all exceed 300ms+.

If I send multiple HTTP2 requests, the first one exceeds ~300ms, and every request after is ~0ms, but I get errors on the server saying some connections could not be handled.

The same scenarios occur even on --release and whether I call through cargo or not.

use std::net::{SocketAddr};
use std::sync::Arc;
use std::{fs, io};
use std::any::{Any};
use std::convert::Infallible;
use std::future::Future;
use std::str::FromStr;
use http_body_util::{Full};
use hyper::body::{Body, Bytes};
use hyper::server::conn::{http2};
use hyper::service::{Service, service_fn};
use hyper_util::rt::{TokioIo};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::{ServerConfig};
use tokio::net::TcpListener;
use tokio_rustls::{rustls, TlsAcceptor};
use crate::exchange::{ExchangeContext, };
use crate::handler::Handler;

#[derive(Clone)]
pub struct TokioExecutor;
impl<F> hyper::rt::Executor<F> for TokioExecutor
    where
        F: std::future::Future + Send + 'static,
        F::Output: Send + 'static,
{
    fn execute(&self, fut: F) {
        tokio::task::spawn(fut);
    }
}

fn load_certs(filename: &str) -> io::Result<Vec<CertificateDer<'static>>> {
    let public_cert = fs::File::open(filename)
        .map_err(|e| {
            error(format!("failed to open {}: {}", filename, e))
        })?;
    let mut reader = io::BufReader::new(public_cert);
    rustls_pemfile::certs(&mut reader).collect()
}

fn load_private_key(filename: &str) -> io::Result<PrivateKeyDer<'static>> {
    let private_key = fs::File::open(filename).map_err(|e| {
        error(format!("failed to open {}: {}", filename, e))
    })?;
    let mut reader = io::BufReader::new(private_key);
    rustls_pemfile::private_key(&mut reader).map(|key| match key {
        Some(x) => x,
        None => panic!("Could not load private key: '{}'", filename),
    })
}

fn main() {
    if let Err(e) = run_server() {
        eprintln!("FAILED: {:?}", e);
        std::process::exit(1);
    }
}

fn error(err: String) -> io::Error {
    io::Error::new(io::ErrorKind::Other, err)
}

#[tokio::main]
async fn run_server() -> Result<(), ()> {
    let port = 8080;
    let addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
    let server_certs = match load_certs("./server.pem") {
        Ok(certs) => certs,
        Err(_) => todo!()
    };
    let key = match load_private_key("./server.rsa") {
        Ok(key) => key,
        Err(_) => todo!()
    };
    println!("Starting to serve on https://{}", addr);
    let incoming = match TcpListener::bind(&addr).await {
        Ok(incoming) => incoming,
        Err(_) => todo!()
    };
    let mut server_config = match ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(server_certs, key)
        .map_err(|e| error(e.to_string())) {
            Ok(config) => config,
            Err(_) => todo!()
    };
    server_config.alpn_protocols = vec![b"h2".to_vec(),b"http/1.1".to_vec()];
    let server_tls_config = Arc::new(server_config);
    let acceptor = TlsAcceptor::from(server_tls_config.clone());
    loop {
        let (tcp_stream, _remote_addr) = match incoming.accept().await {
            Ok(stream) => stream,
            Err(_) => todo!()
        };
        let tls_acceptor = acceptor.clone();

        match tls_acceptor.accept(tcp_stream).await {
            Ok(tls_stream) => {
                let io = TokioIo::new(tls_stream);
                tokio::task::spawn(async move {
                    if let Err(err) = http2::Builder::new(TokioExecutor).serve_connection(io, service_fn(hello)).await {
                        eprintln!("failed to serve connection: {:#}", err);
                    }
                });
            },
            Err(err) => {
                eprintln!("failed to perform tls handshake: {err:#?}");
                return Err(());
            }
        };
    }
}
async fn hello(_: hyper::Request<hyper::body::Incoming>) -> Result<hyper::Response<Full<Bytes>>, Infallible> {
    Ok(hyper::Response::new(Full::new(Bytes::from("Hello, World!"))))
}

[dependencies]
hyper = { version = "1.5.1", features = [ "http1", "http2", "server" ] }
http-body-util = "0.1.2"
hyper-util = { version = "0.1.10", features = ["full"] }
tokio = { version = "1.42.0", features = ["full"] }
tokio-rustls = "0.26.1"
rustls = { version = "0.23.19", features = ["tls12"] }
rustls-pemfile = "2.2.0"

I can't check it, because your code is incomplete without the use statements and the deps you use.

1 Like

I updated the post with my use + dependencies.

Whoops!

Are you building with --release?

They said:

I managed to fix this issue, which was caused by the client rather than the server.

I used tracing and tracing_subscriber on my client and server and found that my client preferred to use IPv6 over IPv4 when resolving localhost. I changed the priority and now my request times are very much normal.

I had to use localhost over 127.0.0.1 because of sni hostname verification.

from ~300ms --> ~19ms.

4 Likes