wrk Benchmark
Installation & Basic Usage
# Install wrk
# macOS: brew install wrk
# Ubuntu: sudo apt install wrk
# Build from source:
# git clone https://github.com/wg/wrk.git
# cd wrk && make
# Basic benchmark
wrk -t2 -c10 -d30s http://localhost:3000/
# Flags:
# -t number of threads (typically 2x CPU cores)
# -c total number of connections
# -d duration (s, m, h suffix: 30s, 2m, 1h)
# -H extra request header
wrk -t4 -c100 -d30s http://localhost:3000/api/users
wrk -t8 -c200 -d1m -H "Authorization: Bearer token" https://api.example.com/
# Rule of thumb: connections per thread = c / t
# -t4 -c100 means 25 connections per thread
Lua Scripts — Custom Requests
-- post.lua — POST JSON request
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.body = '{"username":"testuser","password":"testpass"}'
-- Run with script:
-- wrk -t4 -c50 -d30s -s post.lua http://localhost:3000/api/login
---
-- headers.lua — dynamic headers
function request()
local headers = {}
headers["Authorization"] = "Bearer " .. os.getenv("API_TOKEN")
headers["X-Request-ID"] = math.random(1, 100000)
return wrk.format(nil, nil, headers, nil)
end
---
-- multiple_paths.lua — random endpoints
paths = {"/api/users", "/api/products", "/api/orders"}
request = function()
path = paths[math.random(#paths)]
return wrk.format("GET", path)
end
Advanced Lua Scripts
-- auth_and_post.lua — login then use token
local token = ""
setup = function(thread)
-- runs once per thread
end
init = function(args)
local body = '{"username":"admin","password":"secret"}'
local headers = {["Content-Type"] = "application/json"}
local resp = wrk.format("POST", "/api/login", headers, body)
-- Note: wrk doesn't support sequential init requests natively
-- Use init to set per-thread state
end
request = function()
return wrk.format("GET", "/api/protected", {
["Authorization"] = "Bearer " .. token,
}, nil)
end
response = function(status, headers, body)
if status ~= 200 then
io.write("Error: " .. status .. "\n")
end
end
done = function(summary, latency, requests)
io.write("-----\n")
for _, p in pairs({50, 75, 90, 99, 99.9}) do
n = latency:percentile(p)
io.write(string.format(" %gth percentile: %0.2f ms\n",
p, n / 1000))
end
end
Interpreting wrk Output
Running 30s test @ http://localhost:3000/
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 12.34ms 4.56ms 234.78ms 89.23%
Req/Sec 802.45 121.32 1.10k 66.00%
Latency Distribution
50% 11.23ms ← P50 median
75% 13.45ms
90% 17.89ms ← P90
99% 34.56ms ← P99
96,294 requests in 30.10s, 21.20MB read ← total requests
Socket errors: connect 0, read 0, write 0, timeout 12
Requests/sec: 3198.47 ← throughput (RPS)
Transfer/sec: 720.63KB ← bandwidth
# Key metrics:
# Latency Avg — mean response time (affected by outliers)
# Latency 99% — 99th percentile (worst 1% of responses)
# Req/sec — throughput
# Socket errors — connection problems (bad sign if > 0)
wrk2 — Constant Throughput Mode
# wrk2 adds rate limiting for more realistic load
# brew install wrk2
# git clone https://github.com/giltene/wrk2
# Test at exactly 1000 RPS
wrk2 -t4 -c100 -d30s -R 1000 http://localhost:3000/
# Test at 500 RPS to observe behavior under controlled load
wrk2 -t2 -c50 -d60s -R 500 http://localhost:3000/api/search
# wrk2 produces HDR histogram latencies
# Better for measuring latency at specific load levels
# Use wrk for finding max throughput
# Use wrk2 for measuring latency at target RPS
Flag Quick Reference
| Flag | Description | Example |
|---|---|---|
-t | Number of threads | -t4 |
-c | Total connections | -c100 |
-d | Duration | -d30s, -d2m |
-s | Lua script file | -s post.lua |
-H | HTTP header | -H "Auth: Bearer x" |
--latency | Print latency stats | --latency |
--timeout | Socket timeout | --timeout 5s |
-R (wrk2) | Target RPS | -R1000 |