Module 1: Performance Mindset & Benchmarking

`wrk`: fast HTTP load testing

Capsule description

pgbench tells you how fast PostgreSQL runs in isolation. But your users don't speak SQL — they speak HTTP. What they feel is the sum of the network, your FastAPI app, the ORM, the connection pool, the DB, JSON serialization and the trip back. To measure that sum you need a tool that generates real HTTP load with high concurrency and reports honest percentiles.

wrk is that tool. It's what most SREs and senior backend devs use when they need a fast, serious benchmark without standing up a testing infrastructure. It's written in C, it's surprisingly efficient, and it reports the full latency histogram from the very first command.

In this capsule you'll install wrk, read its output properly, distinguish three types of tests (smoke / load / stress), and optionally write Lua scripts for slightly more complex scenarios. By the end, you'll be able to spin up an HTTP benchmark in 30 seconds and produce defensible numbers for your BENCHMARKS.md.


Why wrk (and not ab, hey, k6, JMeter)

There are several HTTP load testing tools. wrk earned its place because it's:

  • Fast and efficient. A single wrk process can generate thousands of RPS from a laptop without saturating.
  • It reports percentiles from the very first command (unlike Apache's ab, which is still common but doesn't report percentiles well).
  • Simple. A single command line to get started. No config files needed.
  • Scriptable (Lua) when you need something more complex: variables, dynamic headers, POST requests with a body.
  • Cross-platform. Works on macOS, Linux, WSL.

A quick comparison with the competition:

ToolWhen it's better than wrk
ab (Apache Bench)Almost never. It's limited and reports little. Useful only for a quick "smoke test" if you have nothing else.
heyEquivalent, written in Go. More readable output for some. Same idea.
k6When you need VERY complex scenarios (auth flows, dependencies between requests) and you're willing to invest in learning a DSL.
locustWhen the scenarios are complex but you want to write them in Python (next capsule).
JMeterOnly if your team already uses it. Heavy and UI-oriented.

Informal rule: start with wrk. If something's missing, move up to locust. If you need even more, move up to k6. Most of the time wrk is enough.


Installation

macOS (Homebrew)

brew install wrk

wrk --version
# wrk 4.2.0 [kqueue] Copyright (C) 2012 Will Glozer

Linux (Debian/Ubuntu)

sudo apt-get install build-essential libssl-dev git -y
git clone https://github.com/wg/wrk.git wrk
cd wrk
make
sudo cp wrk /usr/local/bin

wrk --version
# wrk 4.2.0 [epoll] ...

Verify the installation

Point it at any public endpoint (a lightweight site):

wrk -t2 -c10 -d10s https://httpbin.org/get

If you see output with "Requests/sec" and a "Latency Distribution", it's installed correctly.


Anatomy of a wrk command

wrk -t4 -c50 -d30s http://localhost:8000/books
FlagMeaningHow to choose it
-t<N>wrk's threads (not your app's).Rule: ~1 thread per physical core of your client machine. For a typical laptop, -t4 is fine.
-c<N>Concurrent connections held open.Start with the concurrency you expect in production (50-100 is a reasonable starting point).
-d<duration>Test duration (30s, 1m, 5m).Minimum 30s for the average to stabilize. 60s-120s for serious baselines.

The URL goes at the end. It can include query params (?author=tolkien).

Other useful flags you'll see later:

FlagMeaning
--latencyPrints the detailed percentile distribution (p50/75/90/99/99.9/99.99). You almost always want it.
--timeout <N>Timeout per request (default: 2s). Raise it if your endpoint is very slow (--timeout 30s).
-s <script>Loads a Lua script to customize requests.
-H <header>Adds a header (can be repeated: -H "Auth: Bearer X" -H "X-Custom: Y").

Your first benchmark with wrk

We'll use the same minimal API from capsule 03. If you closed it, bring it back up:

# api_demo.py
from fastapi import FastAPI
import time
import random

app = FastAPI()

@app.get("/fast")
def fast():
    return {"ok": True}

@app.get("/slow")
def slow():
    if random.random() < 0.05:
        time.sleep(random.uniform(1.0, 2.0))
    else:
        time.sleep(0.05)
    return {"ok": True}
uvicorn api_demo:app --workers 4

In another terminal:

# Quick smoke test — 5 seconds to see if the API responds
wrk -t2 -c10 -d5s http://localhost:8000/fast

Expected output:

Running 5s test @ http://localhost:8000/fast
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.85ms    1.20ms  35.40ms   95.20%
    Req/Sec     2.74k   320.50     3.50k    72.50%
  27510 requests in 5.00s, 3.85MB read
Requests/sec:   5502.00
Transfer/sec:    789.45KB

How to read that output

LineWhat it tells you
2 threads and 10 connectionsYour wrk config.
Latency: Avg ... Stdev ... Max ... +/- StdevBasic latency statistics. Careful: it's an average, not percentiles. The +/- Stdev indicates what % of requests fell within ±1 stddev of the average.
Req/SecRequests per second per thread. Multiply by threads to get the total.
27510 requests in 5.00sTotal requests processed.
Requests/sec: 5502.00The real throughput. This is your API's TPS.
Transfer/secHow many bytes per second traveled (response size × RPS).

What's missing: percentiles. That's why you almost always add --latency.

With --latency to see percentiles

wrk -t2 -c10 -d5s --latency http://localhost:8000/fast

Additional output:

  Latency Distribution
     50%    1.65ms
     75%    2.10ms
     90%    2.95ms
     99%    8.50ms

Now that's useful. Now you can say: "p50=1.65ms, p99=8.5ms". Compare with capsule 02: the difference between p50 and p99 is 5x — a consistent distribution, not bimodal.


The three types of tests with wrk

The three classic load testing modes. Each one answers a different question.

1. Smoke test (5-10s)

Question: does the API respond without errors under minimal load?

wrk -t2 -c5 -d5s --latency http://localhost:8000/fast
  • Low concurrency (5-10).
  • Short duration (5s).
  • Goal: verify that the endpoint responds, without measuring anything serious.
  • Useful after a deploy to confirm the API is alive.

2. Load test (30s-2m)

Question: what percentiles does the API have under representative load?

wrk -t4 -c50 -d60s --latency http://localhost:8000/books
  • Concurrency that reflects production (~50-100 typically).
  • Duration long enough to stabilize (60s+).
  • Goal: the real baseline. What you report in BENCHMARKS.md.

3. Stress test (find the breaking point)

Question: how much load does the API take before degrading or failing?

# Ramp up concurrency progressively
for c in 50 100 200 400 800 1600; do
  echo "=== c=$c ==="
  wrk -t4 -c$c -d30s --latency http://localhost:8000/books
done
  • Increasing concurrency until something breaks (errors, timeouts, unacceptable latency).
  • Goal: find the "knee" — the point where RPS stops scaling and latency explodes.

What to watch in the output:

  • As long as RPS grows and latency holds: your app is coping.
  • When RPS stops growing (or falls) and latency doubles: you found the knee.
  • When you see Socket errors: ... timeout ...: you exceeded capacity.

Important: a stress test tells you how much your app takes on this hardware with this config. Change hardware, workers, pool, and the answer changes. That's why you always document the context.


Worked example: a load test with a reproducible baseline

Let's do a "serious" load test — with warmup, multiple runs, correct aggregation.

Setup

mkdir wrk-baseline
cd wrk-baseline

A bash script that applies capsule 03's discipline

#!/bin/bash
# bench/wrk-baseline.sh
set -euo pipefail

URL="${1:-http://localhost:8000/fast}"
THREADS=4
CONNECTIONS=50
DURATION="60s"
WARMUP_DURATION="30s"
RUNS=5

echo "=== Benchmark of $URL ==="
echo "Threads: $THREADS  Connections: $CONNECTIONS  Duration: $DURATION  Runs: $RUNS"
echo ""

echo "[warmup] Running warmup ($WARMUP_DURATION) — discarded..."
wrk -t$THREADS -c$CONNECTIONS -d$WARMUP_DURATION "$URL" > /dev/null

for i in $(seq 1 $RUNS); do
  echo ""
  echo "[run $i/$RUNS]"
  wrk -t$THREADS -c$CONNECTIONS -d$DURATION --latency "$URL" \
    | grep -E "(Requests/sec|Latency Distribution|50%|75%|90%|99%)"
done

Make it executable and use it:

chmod +x bench/wrk-baseline.sh
./bench/wrk-baseline.sh http://localhost:8000/slow

Expected output:

=== Benchmark of http://localhost:8000/slow ===
Threads: 4  Connections: 50  Duration: 60s  Runs: 5

[warmup] Running warmup (30s) — discarded...

[run 1/5]
  Latency Distribution
     50%   54.20ms
     75%   58.80ms
     90%  102.10ms
     99% 1820.40ms
Requests/sec:    910.30

[run 2/5]
  Latency Distribution
     50%   53.80ms
     75%   57.50ms
     90%   98.20ms
     99% 1780.20ms
Requests/sec:    915.40

[run 3/5]
  ... (similar)

[run 4/5]
  ... (similar)

[run 5/5]
  ... (similar)

What's right about it:

  • ✅ Explicit warmup (30s discarded).
  • ✅ Multiple runs (5).
  • ✅ Each run reports percentiles and RPS.
  • ✅ We filter only what's relevant with grep.

What's still missing to reach the final BENCHMARKS.md: aggregating the percentiles across runs (the median). You can do that by copying the numbers into a spreadsheet or with a Python script that parses the output.


Lua scripts for slightly more complex scenarios

Sometimes you need more than a simple GET. For example: POST requests with a body, dynamic headers, path randomization. wrk supports Lua scripts for this.

Case: rotating query params to avoid an artificial cache hit

If you run wrk -d60s http://localhost:8000/books?author=tolkien, every request asks for the same author. Your DB caches the first one and the rest fly. That lies in your favor.

To vary the query param:

-- random_author.lua
authors = {"tolkien", "asimov", "le_guin", "herbert", "orwell"}

request = function()
  local author = authors[math.random(#authors)]
  local path = string.format("/books?author=%s", author)
  return wrk.format("GET", path)
end

Use it:

wrk -t4 -c50 -d60s -s random_author.lua --latency http://localhost:8000

Each request goes with a random author from the 5. Realistic cardinality, no artificial cache hit.

Case: POST with a JSON body

-- post_book.lua
wrk.method = "POST"
wrk.body   = '{"title":"My Book","author_id":42}'
wrk.headers["Content-Type"] = "application/json"
wrk -t4 -c50 -d60s -s post_book.lua --latency http://localhost:8000/books

Case: dynamic header (rotating auth token)

-- auth_rotating.lua
tokens = {"token_a", "token_b", "token_c"}

request = function()
  local t = tokens[math.random(#tokens)]
  wrk.headers["Authorization"] = "Bearer " .. t
  return wrk.format("GET", "/me")
end

When NOT to use Lua scripts:

  • If your scenario has multiple sequential steps (login → POST → GET → DELETE in a given order), wrk isn't the tool. Jump to locust (next capsule).
  • If you need complex conditional logic (wait for a response, parse JSON, decide the next request), also locust.

wrk shines with stateless loads: each request is independent and can be parallelized to the max.


Coordinated omission (the famous trap)

There's a well-known criticism of wrk (and of almost every load testing tool) called coordinated omission, formalized by Gil Tene. It's worth knowing.

The problem: when a request is slow, the same client's subsequent requests wait for it to finish before starting. That makes the client "stop measuring" during those slow moments. Result: the reported latencies can be underestimated because the "victims" of the slowdown aren't fully counted.

How it affects wrk and wrk2:

  • wrk has this problem in its original form.
  • wrk2 (a fork by Gil Tene) corrects it by sending requests at a fixed rate independent of the responses, which reproduces the more realistic "open loop" scenario.

When you should care:

  • If your app has highly variable latencies (p99 much greater than p50): wrk may under-report the real p99.
  • If your app is consistent (p99 close to p50): the effect is smaller.
  • For "casual" local benchmarks, wrk is fine. For production benchmarks where you report to stakeholders, consider wrk2 or k6 (which also handle it).

Heuristic: if wrk reports your p99 as "not that bad" but users are complaining, suspect coordinated omission. Switch to wrk2 and compare.


Why does this matter in real work?

1. Confirming that a fix worked. You optimized an endpoint. Before, wrk reported p99=2,000ms. After, p99=180ms. That difference, measured with the same tool and the same parameters, is evidence for the PR, for the team, for your portfolio.

2. Capacity prediction. The product team asks "we should handle 500 RPS for Black Friday". You run a stress test scaling concurrency. If at 500 RPS p99 is 200ms and there are no errors: you're fine. If at 500 RPS p99 is 5s or timeouts appear: you need to scale first.

3. Detecting regressions in CI. A mature pattern: run wrk against a PR's version of the app vs the version on main. If p99 rises >20%, CI flags a regression and blocks the merge. It's what companies with a strong performance culture do (Cloudflare, Stripe, etc.).


Traps and common mistakes

Mistake 1 (practical): running wrk from the same machine as the app

Symptom: the latency you report is 2x lower than the real one because the network doesn't exist.

Why it's problematic: localhost is ~0.05ms of RTT. A real network (same datacenter ~1ms, different regions ~50ms) changes the numbers materially. For benchmarks that are representative of production, run wrk from another machine (another VM, another container, another region).

How to fix it: always document the topology. If you're going to measure against real production, run wrk from a machine similar (in terms of network latency) to your users'.

Mistake 2 (conceptual): thinking --latency reports p99.9 or p99.99

Symptom: wrk --latency reports up to p99. What about p99.9?

Why it matters: for high-scale systems (millions of requests), p99 is the "worst 1%", which is still a lot of users. p99.9 is more representative for large platforms.

How to get finer percentiles: wrk has the -L or --latency option (the exact format depends on the version); in wrk2 you can request -R (rate) and get a more detailed histogram. For serious analysis, export the raw latencies and process them with HdrHistogram.

Mistake 3 (practical): a very short timeout causes false "errors"

Symptom: wrk reports Socket errors: connect 0, read 0, write 0, timeout 30. You think your app is failing.

Why it happens: the --timeout default is 2s. If your endpoint takes longer, wrk counts it as a timeout. It's not that the API "failed" — it's that wrk didn't wait long enough.

How to fix it: adjust --timeout to a value representative of your app's SLA. If your endpoint can legitimately take 10s, use --timeout 30s so wrk measures instead of giving up.

Mistake 4 (conceptual): assuming "Avg" is the p50

Symptom: you read the output Latency Avg 50ms and think "the p50 is 50ms".

Why it's wrong: the Latency: Avg ... Stdev ... Max line is average + standard deviation, NOT percentiles. The p50 (median) can be very different from the average if there's a bimodal distribution (capsule 02). For percentiles you need --latency.

How to fix it: always run with --latency and report the Latency Distribution, not the Avg.

Mistake 5 (practical): few threads, high concurrency, a machine without resources

Symptom: you run wrk -t1 -c1000 ... and see that wrk itself is using 100% CPU. Your numbers don't reflect the app — they reflect that wrk is saturated.

Why it happens: a single wrk thread can't handle 1000 connections efficiently. The informal rule is: t >= physical cores and c <= ~50 * t.

How to fix it: on a laptop with 8 cores, -t8 -c400 is reasonable. If you need more, distribute the load (several machines running wrk in parallel).


Exercises

Exercise 1: Your first load test

Bring up the demo API (api_demo.py) and run a 30s load test against /fast with 4 threads and 50 connections. Report p50, p95, p99 and RPS.

See solution
# Make sure the API is running
uvicorn api_demo:app --workers 4 &
sleep 2

# Load test
wrk -t4 -c50 -d30s --latency http://localhost:8000/fast

Expected output (varies by hardware):

Running 30s test @ http://localhost:8000/fast
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.85ms    1.45ms  45.20ms   89.50%
    Req/Sec     4.32k   480.20     5.10k    74.20%
  Latency Distribution
     50%    2.50ms
     75%    3.20ms
     90%    4.10ms
     99%    8.50ms
  517210 requests in 30.00s, 72.30MB read
Requests/sec:  17240.40

Report:

  • p50: 2.5ms
  • p95: ~5ms (between 90 and 99)
  • p99: 8.5ms
  • RPS: ~17,240

Analysis: consistent distribution (p99 / p50 ≈ 3.4x), high throughput. This API on this hardware comfortably handles 17k RPS for /fast.

Exercise 2: Compare /fast vs /slow

Run the same load test against /fast and /slow. Compare and explain the differences in RPS and in the shape of the percentile distribution.

See solution
echo "=== /fast ==="
wrk -t4 -c50 -d30s --latency http://localhost:8000/fast \
  | grep -E "(50%|75%|90%|99%|Requests/sec)"

echo "=== /slow ==="
wrk -t4 -c50 -d30s --latency http://localhost:8000/slow \
  | grep -E "(50%|75%|90%|99%|Requests/sec)"

Example output:

=== /fast ===
     50%    2.50ms
     75%    3.20ms
     90%    4.10ms
     99%    8.50ms
Requests/sec:  17240.40

=== /slow ===
     50%   54.20ms
     75%   58.80ms
     90%  102.30ms
     99% 1820.40ms
Requests/sec:    915.20

Analysis:

  • /fast: tight distribution, p99 ~3.4x p50. Unimodal. High RPS (~17k).
  • /slow: p50=54ms (close to the simulated time.sleep(0.05)), but p99=1820ms (~34x the p50). A bimodal distribution — confirming the pattern we planted on purpose (5% of requests with sleep(1-2s)). RPS falls to 915 because each request hogs a connection for 50ms or more.

Lesson: the mere presence of 5% slow requests collapses throughput. That's one of the reasons p99 matters: slow requests don't just affect "those users" — they also consume resources that delay everyone.

Exercise 3: Stress test, find the knee

Run a stress test against /fast scaling concurrency: c=10, 50, 100, 250, 500, 1000. Note RPS and p99 at each level. Where's the knee?

See solution
for c in 10 50 100 250 500 1000; do
  echo "=== c=$c ==="
  wrk -t4 -c$c -d20s --latency http://localhost:8000/fast \
    | grep -E "(99%|Requests/sec)"
done

Example output (typical M1 laptop):

=== c=10 ===
     99%    3.20ms
Requests/sec:  10240.50

=== c=50 ===
     99%    8.50ms
Requests/sec:  17240.40

=== c=100 ===
     99%   18.20ms
Requests/sec:  18550.30

=== c=250 ===
     99%   85.30ms
Requests/sec:  17890.10

=== c=500 ===
     99%  220.50ms
Requests/sec:  16240.80

=== c=1000 ===
     99%  580.10ms
Requests/sec:  14820.40

Analysis:

  • RPS grows from c=10 (10k) to c=100 (~18.5k), saturating there.
  • At c=250, RPS starts to drop slightly.
  • At c=1000, RPS fell ~20% vs the peak AND p99 exploded 64x (from 8.5ms → 580ms).

Knee: between c=100 and c=250. More concurrency adds no capacity — it only adds queueing.

Lesson: raising connections beyond the knee doesn't give you more performance. It gives you a worse experience (high latency) and equal or lower throughput. In module 6 you'll learn to size the FastAPI app's pool taking this pattern into account.

Exercise 4: Eliminate artificial cache hits with Lua

Write a Lua script that rotates 10 different query param values for /fast?id=N (assuming your endpoint accepts id even if it doesn't use it). Run wrk with the script and compare RPS against a "monolithic" run (always the same query param). Is there a difference?

See solution

Lua script:

-- random_id.lua
ids = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

request = function()
  local id = ids[math.random(#ids)]
  return wrk.format("GET", "/fast?id=" .. id)
end

Runs:

echo "=== monolithic (?id=1 always) ==="
wrk -t4 -c50 -d20s --latency "http://localhost:8000/fast?id=1" \
  | grep -E "(99%|Requests/sec)"

echo "=== with random ==="
wrk -t4 -c50 -d20s -s random_id.lua --latency http://localhost:8000 \
  | grep -E "(99%|Requests/sec)"

Typical result (on this trivial endpoint):

For /fast the difference is minimal (it touches neither DB nor cache). But the pattern is important.

When it really matters: if the endpoint ran SELECT * FROM products WHERE id = $1, the monolithic version would cache row id=1 in shared_buffers and fly. The random version forces the planner to read 10 different rows, measuring the real case.

Lesson: whenever your benchmark goes to a layer with a cache (PostgreSQL, Redis, CDN), randomize to avoid numbers that lie in your favor.

Exercise 5: Document the result in BENCHMARKS.md

Take exercise 3 and write the corresponding section of BENCHMARKS.md. Include context, methodology, a table with the stress test data, and at least one actionable observation.

See solution
## Stress test — /fast endpoint

### Context

- **Hardware:** MacBook Pro M1, 16GB RAM
- **OS:** macOS 14.5
- **Python:** 3.12.3, FastAPI 0.110.2
- **App server:** `uvicorn --workers 4`
- **Topology:** wrk and app on the same host (localhost)

### Methodology

- Tool: `wrk` 4.2.0
- Variable concurrency: c=10, 50, 100, 250, 500, 1000
- Threads: 4
- Duration: 20s per level
- A single run per level (preliminary; the real baseline will do 5x)
- No explicit warmup (a limitation of this experiment, add it in the final version)

### Results

| Concurrency | RPS | p99 latency |
|-------------|-----|-------------|
| c=10   | 10,240 | 3.2 ms   |
| c=50   | 17,240 | 8.5 ms   |
| c=100  | 18,550 | 18.2 ms  |
| c=250  | 17,890 | 85.3 ms  |
| c=500  | 16,240 | 220.5 ms |
| c=1000 | 14,820 | 580.1 ms |

### Observations

- Maximum sustained capacity: ~18.5k RPS, reached at c=100.
- Beyond c=100, throughput plateaus and latency explodes.
- Knee between c=100 and c=250 → size the FastAPI pool / workers near c=100 for maximum benefit.
- Missing: measure errors (timeouts) at c=1000+; add warmup; run 5 runs per level.

Note: the numbers are illustrative. What matters is the structure: context, methodology, data, actionable observations.

Exercise 6: Use --timeout correctly

Modify the /slow endpoint so the slow operation now takes 5 seconds (not 1-2). Run wrk without touching the default timeout and observe the errors. Now run it with --timeout 10s. What changes?

See solution

Modify api_demo.py:

@app.get("/slow")
def slow():
    if random.random() < 0.05:
        time.sleep(5.0)  # now 5 seconds in the slow case
    else:
        time.sleep(0.05)
    return {"ok": True}

Restart uvicorn.

Without --timeout (default 2s):

wrk -t4 -c50 -d20s --latency http://localhost:8000/slow

Expected output includes:

  Socket errors: connect 0, read 0, write 0, timeout 47
  ...
     99%  1990.20ms   ← p99 capped by the timeout

With --timeout 10s:

wrk -t4 -c50 -d20s --latency --timeout 10s http://localhost:8000/slow

Expected output:

  Socket errors: connect 0, read 0, write 0, timeout 0
  ...
     99%  4980.40ms   ← the real p99

Analysis: without the right timeout, wrk was reporting "47 timeouts" when in reality the app was responding — it just took 5 seconds. And the p99 was "capped" around 2,000ms (the timeout). With --timeout 10s, the numbers reflect reality: the app takes up to ~5s in the worst case.

Lesson: --timeout should be higher than your endpoint's expected worst case. If your SLA is "your endpoint must respond in <30s", use --timeout 30s. If you report "47 timeouts" when the app actually responds slowly but does respond, you're reporting a bug that doesn't exist.


Summary and next step

In this capsule you learned:

  • wrk is the canonical HTTP load testing tool for fast, serious benchmarks. It generates concurrent load and reports percentiles.
  • Basic command: wrk -t<threads> -c<conns> -d<duration> --latency <URL>. Almost always --latency to see percentiles.
  • Three types of tests: smoke (5s, sanity check), load (60s+, baseline), stress (scale concurrency to find the knee).
  • Lua scripts for slightly more complex scenarios (POST, headers, randomization). For truly complex scenarios, use locust.
  • Coordinated omission is a real limitation: for serious production benchmarks, consider wrk2.
  • Common traps: measuring from localhost, reading "Avg" as p50, a too-low timeout, a lack of variation that inflates the cache hit rate.

Before moving on you should be able to:

  • Spin up wrk against any endpoint in 30 seconds
  • Distinguish smoke / load / stress and pick the right mode for each situation
  • Read percentiles from the output and report them in BENCHMARKS.md
  • Identify your API's knee by varying concurrency

Next capsule — locust. wrk is perfect for stateless loads (each request independent). But the real world has flows: login → browse → buy → logout. For multi-step scenarios with conditional logic, written in Python, you use locust. It's the last tool in module 1's kit.


Resources

  1. wrk — official repository on GitHub — source code, Lua scripting documentation, examples.
  2. wrk2 — Gil Tene's fork with a coordinated omission fix — for serious benchmarks where p99 matters a lot.
  3. Gil Tene — "How NOT to Measure Latency" — the talk that explains coordinated omission and why it matters.
  4. Will Glozer — wrk Wiki: scripting — the official reference for wrk's Lua API.
  5. Marc Brooker — "Open Loop and Closed Loop Load Generators" — a technical explanation of open vs closed loop testing.
  6. HdrHistogram — the canonical data structure for reporting latencies with high percentiles (p99.9, p99.99).
  7. k6 documentation — a modern alternative to wrk with JavaScript scripting, useful when scenarios get very complex.

Module 1 — Database Performance & Query Tuning Guide