Send many requests over one HTTP/2 connection

For Rust developers sending lots of requests to the same server at once

You check that fibre_http picked HTTP/2 for a server that offers it, then send 20 requests at the same time and see that they all share one connection.

Before you start

You need Rust with Cargo. fibre_http comes from crates.io:

fibre_http = "0"

Set up the project#

Start a new binary crate.

cargo new many_requests
cd many_requests

Put this in the [dependencies] section of Cargo.toml:

Cargo.toml
[dependencies]
fibre_http = "0"
tokio = { version = "1", features = ["full"] }

The program#

Replace src/main.rs with this:

src/main.rs
use fibre_http::{AsyncClient, Pool};

#[tokio::main]
async fn main() -> Result<(), fibre_http::Error> {
    let pool = Pool::builder().build();
    let client = AsyncClient::builder().pool(&pool).build_async()?;

    let response = client.get("https://httpbin.org/get").send().await?;
    println!("version: {:?}", response.version());
    response.bytes().await?;

    let mut tasks = Vec::new();
    for i in 0..20 {
        let client = client.clone();
        tasks.push(tokio::spawn(async move {
            let url = format!("https://httpbin.org/anything/{i}");
            client.get(url).send().await?.error_for_status()?.bytes().await
        }));
    }
    for task in tasks {
        task.await.expect("task panicked")?;
    }
    println!("21 requests, connections opened: {}", pool.connections_opened());
    Ok(())
}

Run it:

$ cargo run
version: HTTP/2.0
21 requests, connections opened: 1

What each part does#

When a client connects over TLS it tells the server it speaks HTTP/2 and HTTP/1.1. The server picks one. httpbin.org picks HTTP/2, which version() shows. You do not have to configure anything for this.

An HTTP/1.1 connection carries one request at a time. An HTTP/2 connection carries many at once. So the 20 tasks all go over the connection the first request opened, which is why the pool counts one connection for 21 requests. Over HTTP/1.1 the same program would open a connection for each request that runs at the same time as another.

A cloned AsyncClient shares the pool, which is why each task can take its own clone. A blocking SyncClient on the same pool shares the HTTP/2 connection too.

For a plain http:// server that speaks HTTP/2 without TLS, build the client with http2_prior_knowledge().

Where to go next#

The usage guide covers HTTP/2 keepalive pings and per-host connection limits: https://github.com/excsn/fibre/blob/main/http/README.USAGE.md