Why custom made Tokio HTTP server is slower than framework based like Axum, Actix, Ntex and so on?

⚓ rust    📅 2025-05-13    👤 surdeus    👁️ 2      

surdeus

So I just tried this

use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

const MAX_HEADERS: usize = 32;

#[derive(Debug)]
struct HttpRequest<'a> {
    method: &'a [u8],
    path: &'a [u8],
    version: &'a [u8],
    headers: [(&'a [u8], &'a [u8]); MAX_HEADERS],
    header_count: usize,
    body: &'a [u8],
}

fn parse_http_request<'a>(raw: &'a [u8]) -> Option<HttpRequest<'a>> {
    let mut i = 0;
    let len = raw.len();

    fn next_line(input: &[u8], start: usize) -> Option<(usize, usize)> {
        let mut pos = start;
        while pos + 1 < input.len() {
            if input[pos] == b'\r' && input[pos + 1] == b'\n' {
                return Some((start, pos));
            }
            pos += 1;
        }
        None
    }

    let (line_start, line_end) = next_line(raw, i)?;
    let line = &raw[line_start..line_end];
    i = line_end + 2;

    let mut part_start = 0;
    let mut parts: [&[u8]; 3] = [&[]; 3];
    let mut part_index = 0;
    for pos in 0..line.len() {
        if line[pos] == b' ' && part_index < 2 {
            parts[part_index] = &line[part_start..pos];
            part_index += 1;
            part_start = pos + 1;
        }
    }
    parts[part_index] = &line[part_start..];

    let method = parts[0];
    let path = parts[1];
    let version = parts[2];

    let mut headers: [(&[u8], &[u8]); MAX_HEADERS] = [(&[], &[]); MAX_HEADERS];
    let mut header_count = 0;

    while i + 1 < len {
        if raw[i] == b'\r' && raw[i + 1] == b'\n' {
            i += 2;
            break;
        }

        let (line_start, line_end) = next_line(raw, i)?;
        let line = &raw[line_start..line_end];
        i = line_end + 2;

        if let Some(colon_pos) = line.iter().position(|&b| b == b':') {
            let key = &line[..colon_pos];
            let mut val_start = colon_pos + 1;
            if val_start < line.len() && line[val_start] == b' ' {
                val_start += 1;
            }
            let value = &line[val_start..];
            if header_count < MAX_HEADERS {
                headers[header_count] = (key, value);
                header_count += 1;
            }
        }
    }

    let body = &raw[i..];

    Some(HttpRequest {
        method,
        path,
        version,
        headers,
        header_count,
        body,
    })
}

async fn handle_connection(mut stream: TcpStream) {
    
    let mut buffer = [0u8; 8192];

    match stream.read(&mut buffer).await {
        Ok(n) if n == 0 => return,
        Ok(n) => {
            let data = &buffer[..n];

            if let Some(req) = parse_http_request(data) {
                let response = match req.method {
                    b"GET" => b"HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!" as &[u8],
                    _ => b"HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n",
                };

                let _ = stream.write_all(response).await;
            } else {
                let _ = stream.write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n").await;
            }
        }
        Err(_) => {}
    }
}

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
    println!("Server running on 127.0.0.1:8080");

    loop {
        let (socket, _) = listener.accept().await.unwrap();
        tokio::spawn(handle_connection(socket));
    }
}

It has lower performance than framework based

[root@localhost ~]# wrk -c 250 -d 15 -t 8 http://127.0.0.1:8080
Running 15s test @ http://127.0.0.1:8080
  8 threads and 250 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    70.84ms   67.03ms 508.20ms   75.74%
    Req/Sec   109.73     68.04   330.00     69.81%
  12214 requests in 15.09s, 620.24KB read
  Socket errors: connect 0, read 12180, write 0, timeout 0
Requests/sec:    809.15
Transfer/sec:     41.09KB

With Axum on the same machine my android phone I can get this result :

[root@localhost axum]# wrk -c 250 -d 15 -t 8 http://127.0.0.1:8080
Running 15s test @ http://127.0.0.1:8080
  8 threads and 250 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.50ms    2.05ms  36.43ms   70.96%
    Req/Sec     5.41k   569.34    11.44k    74.41%
  644716 requests in 15.09s, 79.93MB read
Requests/sec:  42723.13
Transfer/sec:      5.30MB

What are the reasons behind that slower performance result?

13 posts - 5 participants

Read full topic

🏷️ rust_feed