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

Project 3: HTTP Load Tester (mini wrk/hey)

Combines: Lessons 18-22 (tokio::sync, tokio::net, task-locals, graceful shutdown, tracing).

What you’ll build

A CLI tool that hammers an HTTP endpoint, controls concurrency with a Semaphore, handles Ctrl+C gracefully, propagates request IDs via task-locals, and reports latency percentiles.

Architecture

┌───────────────────────────────────────────────────────────────┐
│  load-tester --url http://example.com --requests 100 -c 10   │
└──────────────────────────┬────────────────────────────────────┘
                           │
                    ┌──────▼──────┐
                    │  CLI Parser │ (clap)
                    │  --url      │
                    │  --requests │
                    │  --concurrency│
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
     ┌────────▼───┐  ┌────▼─────┐  ┌──▼──────────┐
     │ Semaphore  │  │ Shutdown │  │ Result       │
     │ (c permits)│  │ (Notify) │  │ Collector    │
     │            │  │ Ctrl+C   │  │ (Mutex<Vec>) │
     └────────┬───┘  └────┬─────┘  └──┬──────────┘
              │            │           │
              └────────────┼───────────┘
                           │
              ┌────────────▼────────────────┐
              │  for each request 1..N:      │
              │    tokio::spawn {            │
              │      acquire semaphore       │
              │      set task-local req_id   │
              │      select! {               │
              │        shutdown => return    │
              │        send_request => {     │
              │          record latency      │
              │          record status       │
              │        }                     │
              │      }                       │
              │    }                         │
              └────────────┬────────────────┘
                           │
                    ┌──────▼──────┐
                    │  Report     │
                    │  p50, p90   │
                    │  p99, max   │
                    │  throughput │
                    │  status map │
                    └─────────────┘

CLI interface

load-tester --url https://example.com/api --requests 1000 --concurrency 50
FlagShortDefaultDescription
--url-urequiredTarget URL
--requests-n100Total requests to send
--concurrency-c10Max concurrent requests

Sample output

Target: https://example.com/api
Requests: 1000, Concurrency: 50

Running...  [1000/1000] done

Results:
  Total:      1000 requests
  Succeeded:  985
  Failed:     15
  Duration:   2.34s
  Throughput: 427.35 req/s

Latency:
  p50:    4.2ms
  p90:   12.1ms
  p99:   45.3ms
  max:   102.7ms

Status codes:
  200: 985
  503: 15

Key implementation details

Concurrency with Semaphore

#![allow(unused)]
fn main() {
let sem = Arc::new(Semaphore::new(concurrency));
for i in 0..total_requests {
    let permit = sem.clone().acquire_owned().await?;
    tokio::spawn(async move {
        let result = send_request(&url).await;
        drop(permit); // release slot
        result
    });
}
}

Latency percentiles

#![allow(unused)]
fn main() {
fn percentile(sorted: &[Duration], p: f64) -> Duration {
    let idx = ((sorted.len() as f64) * p / 100.0) as usize;
    sorted[idx.min(sorted.len() - 1)]
}
}

Graceful Ctrl+C

#![allow(unused)]
fn main() {
let shutdown = Arc::new(Notify::new());
tokio::spawn({
    let s = shutdown.clone();
    async move {
        tokio::signal::ctrl_c().await.ok();
        s.notify_waiters();
    }
});
}

Making HTTP requests with raw TCP

Since we don’t have reqwest, we use raw TcpStream with minimal HTTP/1.1:

#![allow(unused)]
fn main() {
async fn http_get(url: &str) -> Result<(u16, Duration)> {
    let start = Instant::now();
    let mut stream = TcpStream::connect((host, port)).await?;
    stream.write_all(format!("GET {path} HTTP/1.1\r\nHost: {host}\r\n\r\n").as_bytes()).await?;
    // Read response, parse status code
    Ok((status_code, start.elapsed()))
}
}

Exercises

Exercise 1: Basic load tester

Implement the full load tester with Semaphore-based concurrency, latency collection, and percentile reporting. Use raw TCP for HTTP requests.

Exercise 2: Ctrl+C graceful shutdown

Add tokio::signal::ctrl_c() handling. On Ctrl+C, stop spawning new requests, let in-flight ones finish, then print partial results.

Exercise 3: Live progress reporting

Print a progress line that updates every 100ms showing completed/total requests and current throughput. Use a separate task with tokio::time::interval.