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
wrkprocess 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:
| Tool | When 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. |
hey | Equivalent, written in Go. More readable output for some. Same idea. |
k6 | When you need VERY complex scenarios (auth flows, dependencies between requests) and you're willing to invest in learning a DSL. |
locust | When the scenarios are complex but you want to write them in Python (next capsule). |
| JMeter | Only 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
| Flag | Meaning | How 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:
| Flag | Meaning |
|---|---|
--latency | Prints 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
| Line | What it tells you |
|---|---|
2 threads and 10 connections | Your wrk config. |
Latency: Avg ... Stdev ... Max ... +/- Stdev | Basic latency statistics. Careful: it's an average, not percentiles. The +/- Stdev indicates what % of requests fell within ±1 stddev of the average. |
Req/Sec | Requests per second per thread. Multiply by threads to get the total. |
27510 requests in 5.00s | Total requests processed. |
Requests/sec: 5502.00 | The real throughput. This is your API's TPS. |
Transfer/sec | How 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),
wrkisn't the tool. Jump tolocust(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:
wrkhas 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):
wrkmay under-report the real p99. - If your app is consistent (p99 close to p50): the effect is smaller.
- For "casual" local benchmarks,
wrkis fine. For production benchmarks where you report to stakeholders, considerwrk2ork6(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 simulatedtime.sleep(0.05)), but p99=1820ms (~34x the p50). A bimodal distribution — confirming the pattern we planted on purpose (5% of requests withsleep(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:
wrkis 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--latencyto 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
wrkagainst 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
- wrk — official repository on GitHub — source code, Lua scripting documentation, examples.
- wrk2 — Gil Tene's fork with a coordinated omission fix — for serious benchmarks where p99 matters a lot.
- Gil Tene — "How NOT to Measure Latency" — the talk that explains coordinated omission and why it matters.
- Will Glozer — wrk Wiki: scripting — the official reference for
wrk's Lua API. - Marc Brooker — "Open Loop and Closed Loop Load Generators" — a technical explanation of open vs closed loop testing.
- HdrHistogram — the canonical data structure for reporting latencies with high percentiles (p99.9, p99.99).
- k6 documentation — a modern alternative to
wrkwith JavaScript scripting, useful when scenarios get very complex.
Module 1 — Database Performance & Query Tuning Guide