Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Benchmarks

aioduct includes criterion benchmarks comparing HTTP client overhead against popular alternatives.

CrateVersionDescription
aioduct0.2.5This crate — hyper 1.x, no hyper-util, async-native
reqwest0.12The most popular Rust HTTP client, built on hyper + hyper-util + tower
hyper-util0.1hyper’s official high-level client (legacy::Client), minimal wrapper
isahc1.8Built on libcurl via curl-sys, independent HTTP stack

Setup

All benchmarks hit a local hyper server on loopback (127.0.0.1), eliminating network latency to isolate pure client overhead. Each benchmark reuses a single client and connection pool across iterations, measuring steady-state performance with warm connections.

Running

# All benchmarks
cargo bench -p aioduct-bench

# All benchmarks through just
just bench

# Run a benchmark group by Criterion name filter
cargo bench -p aioduct-bench --bench bench_main -- e2e_h1
just bench-group e2e_h1/get_small

# Save and compare local Criterion baselines
just bench-save main
just bench-compare main

# Emit bencher-compatible output for continuous benchmarking
cargo bench -p aioduct-bench --bench bench_main \
  -- --output-format bencher --noplot --color never

HTML reports are generated in target/criterion/.

Continuous Benchmarking

The GitHub benchmark workflow runs the full Criterion suite on pushes to main, once per day, and on manual dispatch. It emits bencher-compatible output for benchmark-action/github-action-benchmark, stores benchmark history in the workflow cache, writes a job summary, and reports an alert when a benchmark is more than 200% of the previous result for the same branch. Alerts are informational rather than merge-blocking because absolute loopback timings on GitHub-hosted runners can vary substantially with the assigned host. A regression should be confirmed on a stable machine and against the comparison clients from the same run. After saving the cache, the benchmark workflow triggers the Pages workflow, which restores the latest benchmark cache and publishes the chart dashboard at https://adamcavendish.github.io/aioduct/dev/bench/.

Results

Measured on Linux 5.15, Rust 1.85, release profile. Times are the mean of 30–100 samples (lower is better).

HTTP/1.1 GET Request (bytes)

Simple GET, read entire response as Bytes.

ClientMeanvs aioduct
aioduct43.0 µs
hyper-util44.8 µs+4.2%
reqwest48.6 µs+13.0%
isahc91.3 µs+112.3%

HTTP/1.1 GET Request (text)

GET, read response as UTF-8 String.

ClientMeanvs aioduct
aioduct44.7 µs
reqwest47.5 µs+6.3%

JSON Deserialization

GET + deserialize a small JSON object ({"message":"hello","count":42}).

ClientMeanvs aioduct
aioduct43.6 µs
reqwest47.4 µs+8.7%

POST with 4 KB Body

POST a 4 KB string, read response bytes.

ClientMeanvs aioduct
aioduct53.3 µs
reqwest59.8 µs+12.2%
isahc76.2 µs+43.0%

Large Body Download (64 KB, HTTP/1.1)

GET a 64 KB response, read as bytes.

ClientMeanvs aioduct
hyper-util60.1 µs-4.0%
aioduct62.6 µs
reqwest64.5 µs+3.0%

Large Body Download (1 MB, HTTP/1.1)

GET a 1 MB response, read as bytes.

ClientMeanvs aioduct
aioduct465.8 µs
reqwest481.4 µs+3.3%

10 Concurrent Requests (HTTP/1.1)

10 GET requests dispatched via tokio::spawn, all awaited.

ClientMeanvs aioduct
aioduct124.3 µs
reqwest140.9 µs+13.4%

50 Concurrent Requests (HTTP/1.1)

50 GET requests dispatched via tokio::spawn, all awaited.

ClientMeanvs aioduct
aioduct361.5 µs
reqwest425.0 µs+17.6%

HTTP/2 GET Request

GET via h2c (HTTP/2 over cleartext).

ClientMeanvs aioduct
aioduct61.5 µs
hyper-util84.7 µs+37.7%

HTTP/2 Download (64 KB)

GET a 64 KB response via h2c.

ClientMeanvs aioduct
aioduct105.1 µs
hyper-util2,068 µs+1868%

(hyper-util h2 uses default 64 KB window sizes, hitting flow-control bottlenecks on larger payloads. aioduct configures 2 MB stream / 4 MB connection windows.)

HTTP/2 Download (1 MB)

GET a 1 MB response via h2c (aioduct only).

ClientMean
aioduct734.0 µs

HTTP/2 10 Concurrent Requests

10 concurrent requests multiplexed over a single h2c connection (aioduct only).

ClientMean
aioduct162.1 µs

HTTP/2 POST with 4 KB Body

POST a 4 KB payload via h2c (aioduct only).

ClientMean
aioduct87.7 µs

Connection Pool Overhead

Comparison of pooled vs no-pool (fresh connection per request).

ProtocolWith PoolNo PoolSpeedup
HTTP/1.144.8 µs95.4 µs2.1×
HTTP/280.6 µs191.4 µs2.4×

SSE: Consume 100 Events

Parse 100 Server-Sent Events from a single response (aioduct only).

ClientMean
aioduct65.4 µs

Multipart Upload (small)

Multipart form with two text fields.

ClientMeanvs aioduct
aioduct50.8 µs
reqwest66.6 µs+31.1%

Multipart Upload (1 MB file)

Multipart form with a 1 MB file part.

ClientMeanvs aioduct
aioduct846.3 µs
reqwest944.9 µs+11.7%

Streaming Upload (1 MB)

Stream a 1 MB body to an echo server.

ClientMeanvs aioduct
reqwest750.8 µs-2.6%
aioduct770.9 µs

Chunk Download (1 MB)

Parallel range-based download of a 1 MB file.

ChunksMean
1 chunk2,239 µs
4 chunks2,308 µs
8 chunks2,297 µs
Single GET (baseline)362.5 µs

(On loopback the overhead of multiple range requests exceeds the parallelism benefit. Chunk download shows gains on real networks with higher latency.)

Body Stream (64 KB)

Read a 64 KB response frame-by-frame vs collected as bytes (aioduct only).

MethodMean
bytes collect56.0 µs
frame by frame69.3 µs

Analysis

  • aioduct is the fastest or tied for fastest in most benchmarks, sitting close to raw hyper-util while providing a much higher-level API (connection pooling, redirects, cookies, middleware, retry, etc.).
  • hyper-util (legacy::Client) is close to aioduct in H1 but struggles in H2 due to default flow-control window sizes.
  • reqwest is 3–31% slower than aioduct in most scenarios. The gap widens for concurrent workloads and multipart uploads.
  • isahc is 43–112% slower due to the libcurl FFI boundary and curl’s internal buffering.
  • Connection pooling provides a consistent ~2× speedup over fresh connections for both H1 and H2.

Caveats

  • These benchmarks measure loopback HTTP client overhead only. In real-world usage, TLS handshakes and network latency dominate.
  • reqwest uses native-tls by default (disabled here since we test plain HTTP).
  • isahc uses libcurl which has its own connection pooling; the curl overhead is most visible on small payloads.
  • The H2 comparison is not apples-to-apples: aioduct configures larger flow-control windows. With matching configuration hyper-util would be closer.
  • Results vary by machine, OS, and Rust version. Run the benchmarks yourself for your environment.

Benchmark Suites

SuiteBench FileScenarios
e2e_h1benches/bench_main/e2e_h1.rsHTTP/1.1 GET bytes/text, POST 4K, download 64K/1M
e2e_h2benches/bench_main/e2e_h2.rsHTTP/2 GET, POST 4K, download 64K/1M
e2e_concurrentbenches/bench_main/e2e_concurrent.rsHTTP/1.1 and HTTP/2 concurrent requests
e2e_featuresbenches/bench_main/e2e_features.rsSSE, multipart, upload 1M, chunk download, body stream, JSON
e2e_poolingbenches/bench_main/e2e_pooling.rsHTTP/1.1 and HTTP/2 with-pool vs no-pool
runtimebenches/bench_main/e2e_runtime.rsTokio, smol, and compio runtime comparisons
micro_poolbenches/bench_main/micro_pool.rsPool checkout/check-in and coalescing scans
micro_cookiebenches/bench_main/micro_cookie.rsCookie request application and response storage
micro_bodybenches/bench_main/micro_body.rsBody frame polling through middleware layers