wrk is a modern HTTP benchmarking tool. It generates significant load from a single multi-core machine by combining a multithreaded design with a scalable event-notification system (epoll, kqueue, or event ports). An optional embedded LuaJIT script lets you customize request generation, response processing, and reporting.
This guide shows you how to install wrk, run benchmarks, read the results, and script custom load tests. Every command, option, and value here is taken directly from the wrk source code.
Note
wrkmeasures how fast a server responds under concurrent load. It is a load generator and latency measurement tool, not a functional test framework and not a distributed swarm. Onewrkprocess drives load from one machine.
1. Quick Start
Run a 30-second benchmark using 12 threads and keeping 400 connections open:
wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html
Output:
Running 30s test @ http://127.0.0.1:8080/index.html
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 635.91us 0.89ms 12.92ms 93.69%
Req/Sec 56.20k 8.07k 62.00k 86.54%
22464657 requests in 30.00s, 17.76GB read
Requests/sec: 748868.53
Transfer/sec: 606.33MB
That is the whole workflow: point wrk at a URL, tell it how many threads and connections to use, and how long to run. Everything else is refinement.
Tip
Start with a small connection count and short duration to confirm the target responds, then scale up
-cand-d. The number of connections must always be greater than or equal to the number of threads.
2. Requirements
wrk runs on UNIX-like operating systems. There is no Windows build.
| Requirement | Detail |
|---|---|
| Operating system | Linux, macOS, FreeBSD, or illumos/Solaris (any UNIX-like OS supported by LuaJIT and OpenSSL) |
| Build toolchain | A C compiler and GNU make (gmake on *BSD) |
| Runtime libraries | LuaJIT and OpenSSL — bundled with the source and built automatically, or supplied by your system |
| Privileges | None required to run; you only need network access to the target |
Important
wrkdoes not read any configuration file and does not read any environment variables. All behavior is controlled by command-line options and, optionally, a Lua script. There is nothing to set up in your shell profile.
3. Installation
wrk is distributed as source and built with make. The build bundles compatible versions of LuaJIT and OpenSSL and compiles them statically, so a plain make usually produces a working binary.
Linux
git clone https://github.com/wg/wrk.git
cd wrk
make
macOS
git clone https://github.com/wg/wrk.git
cd wrk
make
FreeBSD / *BSD
git clone https://github.com/wg/wrk.git
cd wrk
gmake
The build produces a single executable named wrk in the project directory. Copy it somewhere on your PATH to use it system-wide:
sudo cp wrk /usr/local/bin/
Verify the build:
wrk --version
wrk 4.2.0 [epoll] Copyright (C) 2012 Will Glozer
The bracketed name (epoll, kqueue, select, or evport) reports which event-notification system this build will use — it is selected automatically for your platform.
Using system libraries
If you are packaging wrk for a distribution, or you prefer your system's LuaJIT and OpenSSL, point the build at them with WITH_LUAJIT and WITH_OPENSSL:
# Use the system version of both libraries on Linux
make WITH_LUAJIT=/usr WITH_OPENSSL=/usr
# Use the Homebrew OpenSSL on macOS
make WITH_OPENSSL=/usr/local/opt/openssl
Note
When building against system OpenSSL, version 1.1.0 or newer is required.
Tip
Some platforms need extra
CFLAGSorLDFLAGS. The top of theMakefilecontains per-OS examples for SunOS, Darwin, Linux, and FreeBSD.
4. Command-Line Options
The full invocation grammar is:
wrk <options> <url>
The <url> is required and must include a scheme and host, for example http://localhost:8080/ or https://example.com/api.
| Option | Long form | Argument | Default | Description |
|---|---|---|---|---|
-c |
--connections |
<N> |
10 |
Total HTTP connections to keep open, spread across all threads |
-d |
--duration |
<T> |
10 (seconds) |
How long to run the test |
-t |
--threads |
<N> |
2 |
Number of OS threads to use |
-s |
--script |
<file> |
— | Load a LuaJIT script (see Scripting) |
-H |
--header |
<header> |
— | Add an HTTP header to each request; repeatable |
--latency |
— | off | Print a detailed latency percentile distribution | |
--timeout |
<T> |
2s |
Record a timeout if no response arrives within this time | |
-v |
--version |
— | — | Print version and the event-system name, then continue |
-h |
--help |
— | — | Print usage |
Units in arguments
Numeric and time arguments accept unit suffixes, so you rarely need to type raw numbers:
- Numeric arguments (
-t,-c) accept SI units:1k= 1,000,1M= 1,000,000,1G= 1,000,000,000. - Time arguments (
-d,--timeout) accept time units:s(seconds),m(minutes),h(hours) — for example2s,2m,2h.
# 2 threads, 1000 connections, run for 5 minutes
wrk -t2 -c1k -d5m http://127.0.0.1:8080/
Warning
The number of connections must be greater than or equal to the number of threads.
wrk -t8 -c4 ...exits withnumber of connections must be >= threads.
Note
Connections are distributed evenly across threads: each thread handles
connections / threads. If the division is not exact, the remainder of connections is not used, so prefer a-cvalue that is a clean multiple of-t.
5. Reading the Output
Every run prints a header, a per-thread statistics block, totals, and throughput.
Running 30s test @ http://127.0.0.1:8080/index.html
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 635.91us 0.89ms 12.92ms 93.69%
Req/Sec 56.20k 8.07k 62.00k 86.54%
22464657 requests in 30.00s, 17.76GB read
Requests/sec: 748868.53
Transfer/sec: 606.33MB
| Line | Meaning |
|---|---|
Running … test @ … |
Confirms the duration and target URL |
N threads and M connections |
The thread/connection configuration in effect |
Latency row |
Per-request latency: average, standard deviation, maximum, and the percentage of samples within one standard deviation of the mean |
Req/Sec row |
Per-thread request rate, sampled in 100 ms windows: average, standard deviation, maximum, and +/- Stdev |
N requests in T, X read |
Total completed requests, actual elapsed time, and total bytes received |
Requests/sec |
Overall throughput across all threads |
Transfer/sec |
Overall data-receive rate |
Tip
Req/Secis a per-thread figure. The headline number for overall throughput isRequests/secat the bottom.
The +/- Stdev column
+/- Stdev is the share of samples that fall within one standard deviation of the mean. For a healthy, consistent server this is high (around 68% or more). A low value signals erratic response times.
Latency distribution (--latency)
Add --latency to print key percentiles:
wrk -t4 -c100 -d10s --latency http://127.0.0.1:8080/
Latency Distribution
50% 590.00us
75% 720.00us
90% 1.05ms
99% 3.20ms
The four reported percentiles are the 50th, 75th, 90th, and 99th. The 99th percentile ("p99") is the most important single number for tail latency: 99% of requests completed at least this fast.
Important
wrkcorrects for coordinated omission — a measurement bias where a stalled server hides its worst latencies because the load generator also stalls. After the run, recorded latencies are adjusted against the expected request interval so that long pauses are reflected in the distribution. This makeswrk's latency numbers more honest than tools that do not correct for it, especially at high percentiles.
Error and status lines
If the run encountered socket-level problems or non-success responses, extra lines appear:
Socket errors: connect 0, read 12, write 0, timeout 5
Non-2xx or 3xx responses: 134
| Line | Meaning |
|---|---|
Socket errors: connect/read/write/timeout |
Counts of low-level connection, read, write, and timeout failures. Only shown if at least one occurred. |
Non-2xx or 3xx responses |
Count of responses with an HTTP status greater than 399 (i.e. 4xx and 5xx). Only shown if at least one occurred. |
Caution
A high
Requests/secpaired with manyNon-2xx or 3xx responsesis misleading — the server may be returning errors quickly rather than serving real traffic. Always check the error lines before trusting the throughput number.
6. Common Tasks
Run a longer, heavier test
wrk -t12 -c400 -d2m --latency https://api.example.com/health
Add custom headers
Repeat -H for each header. This is how you set authorization, content negotiation, or a custom User-Agent:
wrk -t4 -c100 -d30s \
-H "Authorization: Bearer eyJhbGci..." \
-H "Accept: application/json" \
https://api.example.com/v1/items
Note
A
Hostheader is added automatically from the URL if you do not supply one.
Set a request timeout
By default a request is recorded as a timeout if no response arrives within 2 seconds. Raise it for slow endpoints:
wrk -t4 -c100 -d30s --timeout 10s https://slow.example.com/report
Benchmark over HTTPS
Use an https:// URL. wrk negotiates TLS automatically and sends the hostname via SNI:
wrk -t8 -c200 -d1m https://example.com/
Warning
wrkdoes not verify the server's TLS certificate. It will connect to hosts with expired, self-signed, or mismatched certificates without complaint. This is acceptable for benchmarking but meanswrkmust never be used as a tool to validate a server's certificate.
Stop a run early
Press Ctrl+C. wrk catches the interrupt, stops the threads cleanly, and prints the statistics gathered so far.
7. Scripting with Lua
For anything beyond a fixed GET, supply a LuaJIT script with -s. Scripts can change the HTTP method, build dynamic request bodies and paths, inspect responses, add delays, and emit custom reports.
wrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:8080/
The repository ships ready-to-use examples in the scripts/ directory; the sections below are based on them.
The execution model
Each wrk thread runs an independent Lua scripting context, so script globals (counters, caches) are per-thread, not shared. A script runs across three phases:
- setup — once per thread, after the target address is resolved but before load starts.
- running —
init()once, thenrequest()/response()for every request cycle. - done — once at the end, with the aggregated results.
You define behavior by declaring global functions with specific names. All are optional.
| Function | Phase | Purpose |
|---|---|---|
setup(thread) |
setup | Configure a thread before it starts; pass data into it |
init(args) |
running | Initialize a thread; receives extra command-line arguments |
delay() |
running | Return milliseconds to wait before sending the next request |
request() |
running | Return the HTTP request string to send |
response(status, headers, body) |
running | Inspect each response |
done(summary, latency, requests) |
done | Produce a custom final report |
The wrk table
A global table named wrk describes the request and is available to your script:
wrk = {
scheme = "http",
host = "localhost",
port = nil,
method = "GET",
path = "/",
headers = {},
body = nil,
thread = <userdata>,
}
Set fields on it to shape the default request, and call its helper functions:
wrk.format(method, path, headers, body)— build an HTTP request string, filling in any omitted argument from thewrktable.wrk.lookup(host, service)— return a table of all resolved addresses for a host/service pair.wrk.connect(addr)— returntrueif an address (fromwrk.lookup) accepts a connection.
Example: HTTP POST
scripts/post.lua — set the method, body, and a header. Because nothing changes per request, this is a static request, built once:
wrk.method = "POST"
wrk.body = "foo=bar&baz=quux"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
wrk -t2 -c50 -d30s -s post.lua http://127.0.0.1:8080/submit
Example: a different path each request
scripts/counter.lua — defining request() as a function makes requests dynamic (rebuilt every cycle):
counter = 0
request = function()
path = "/" .. counter
wrk.headers["X-Counter"] = counter
counter = counter + 1
return wrk.format(nil, path)
end
Important
A script that only changes the method, path, headers, or body once (a static request) has no measurable performance cost. Building a new request on every cycle with a
request()function, or parsing responses withresponse(), reduces the loadwrkcan generate. Pre-build what you can ininit().
Example: inspect responses and capture a token
scripts/auth.lua — use response() to read a header from one response and apply it to all future requests:
token = nil
path = "/authenticate"
request = function()
return wrk.format("GET", path)
end
response = function(status, headers, body)
if not token and status == 200 then
token = headers["X-Token"]
path = "/resource"
wrk.headers["X-Token"] = token
end
end
Example: add a random delay
scripts/delay.lua — pace each connection by returning a millisecond delay:
function delay()
return math.random(10, 50)
end
Example: HTTP pipelining
scripts/pipeline.lua — concatenate several requests into one static buffer to pipeline them:
init = function(args)
local r = {}
r[1] = wrk.format(nil, "/?foo")
r[2] = wrk.format(nil, "/?bar")
r[3] = wrk.format(nil, "/?baz")
req = table.concat(r)
end
request = function()
return req
end
Example: pass data into threads with setup()
scripts/setup.lua — setup(thread) runs in the main context and can seed per-thread globals via thread:set():
local counter = 1
local threads = {}
function setup(thread)
thread:set("id", counter)
table.insert(threads, thread)
counter = counter + 1
end
function init(args)
requests = 0
responses = 0
print(("thread %d created"):format(id))
end
function request()
requests = requests + 1
return wrk.request()
end
function response(status, headers, body)
responses = responses + 1
end
The thread object passed to setup() supports:
| Operation | Effect |
|---|---|
thread.addr |
Get or set the server address this thread connects to |
thread:get(name) |
Read a global from the thread's running environment |
thread:set(name, value) |
Set a global in the thread's running environment |
thread:stop() |
Stop the thread (only while it is running) |
Note
Only
boolean,nil,number, andstringvalues — or tables containing only those types — can be transferred withget()/set().
Example: a custom CSV report with done()
scripts/report.lua — done() receives the aggregated results and two statistics objects:
done = function(summary, latency, requests)
io.write("------------------------------\n")
for _, p in pairs({ 50, 90, 99, 99.999 }) do
n = latency:percentile(p)
io.write(string.format("%g%%,%d\n", p, n))
end
end
The statistics objects (latency and requests) expose:
| Accessor | Returns |
|---|---|
latency.min |
Minimum value seen |
latency.max |
Maximum value seen |
latency.mean |
Average value |
latency.stdev |
Standard deviation |
latency:percentile(p) |
The value at percentile p |
latency(i) |
The raw value and its count at index i |
#latency |
The number of distinct recorded values |
The summary table contains duration (microseconds), requests, bytes, and an errors sub-table with connect, read, write, status, and timeout counts.
Passing arguments to a script
Anything after a -- separator is passed to your script's init(args) function rather than interpreted by wrk:
wrk -t2 -c10 -d10s -s myscript.lua http://127.0.0.1:8080/ -- arg1 arg2
Tip
Latency and duration values inside scripts are in microseconds; the request rate is in requests per second. Multiply or divide as needed when formatting your own reports.
8. Benchmarking Tips
Getting trustworthy numbers depends as much on your environment as on wrk's flags.
- Provide enough ephemeral ports. The machine running
wrkmust have enough ephemeral ports available, and closed sockets should be recycled quickly. A high connection count can exhaust the port range otherwise. - Raise the server's listen backlog. To absorb the initial connection burst, the target server's
listen(2)backlog should be larger than the number of concurrent connections you are testing. - Keep scripts cheap. Static requests are free; per-request work is not. If you must build requests dynamically, pre-generate as much as possible in
init()and do a quick lookup inrequest(). - Benchmark from a separate machine. Running
wrkand the server on the same host makes them compete for CPU and skews results. - Use
--latencyfor tail analysis. Average latency hides the slow requests that users actually notice. Watch p99.
Tip
Match
-cto a multiple of-tand start-tnear your core count. More threads than cores rarely helps and adds scheduling overhead.
9. Troubleshooting
wrk's errors are short and printed to standard error. The most common ones and their fixes:
Error: number of connections must be >= threads
- Cause: You requested more threads than connections, e.g.
-t8 -c4. - Solution: Raise
-cso it is at least equal to-t. Connections are split across threads, so each thread needs at least one. - Prevention: Set
-cto a clean multiple of-t(e.g.-t4 -c100).
Error: invalid URL: <url>
- Cause: The URL is missing a scheme or host, or is malformed.
wrkrequires both a scheme (http/https) and a host. - Solution: Use a fully qualified URL such as
http://127.0.0.1:8080/path. - Prevention: Always include
http://orhttps://.
Error: unable to connect to <host>:<service> <reason>
- Cause: The target host could not be reached or did not accept a connection during address resolution.
- Solution: Verify the server is running and listening, check the port, and confirm there is no firewall in the way.
- Prevention: Confirm connectivity with
curlorncbefore benchmarking.
Error: unable to resolve <host>:<service> <reason>
- Cause: DNS resolution failed for the host/service pair (raised from
wrk.lookup). - Solution: Check the hostname spelling and your DNS configuration; try an IP address directly.
- Prevention: Use a reachable, resolvable hostname or a literal IP.
Error: unable to initialize SSL
- Cause: TLS context creation failed for an
https://target. - Solution: Confirm
wrkwas built with a working OpenSSL (1.1.0+). Rebuild against system OpenSSL withmake WITH_OPENSSL=/usrif needed. - Prevention: Verify
wrk --versionworks and the binary links a valid OpenSSL.
Error: unable to create thread <N>: <reason>
- Cause: The OS could not create one of the worker threads, usually a resource limit.
- Solution: Lower
-t, or raise your process/thread limits (ulimit). - Prevention: Keep
-tnear your CPU core count.
Error: <file>: <cause> when loading a script
- Cause: The Lua script has a syntax error or could not be loaded.
- Solution: Read the reported cause; fix the script syntax. Test the file with
luajit -bl script.luaif available. - Prevention: Keep scripts small and validate them before a long run.
Error: <message> at <line>:<column> or incomplete request
- Cause: Your script's
request()produced a request that is not valid HTTP.wrkverifies the first request before starting and exits if it cannot be parsed. - Solution: Ensure
wrk.format()(or your hand-built string) produces a complete, well-formed request with proper CRLF line endings. - Prevention: Build requests with
wrk.format()rather than concatenating strings by hand.
Symptom: many Socket errors or timeouts
- Cause: The server is overloaded, the connection count exceeds what it can handle, or the timeout is too low.
- Solution: Reduce
-c, raise--timeout, and confirm the server's listen backlog and ephemeral port range are adequate. - Prevention: Ramp load gradually and watch the error lines.
10. FAQ
Q: Does wrk work on Windows?
No. wrk targets UNIX-like systems (Linux, macOS, the BSDs, illumos/Solaris). There is no Windows build.
Q: Where do I put my settings — a config file or environment variables?
Neither exists. wrk is configured entirely through command-line options and an optional Lua script. It reads no config file and no environment variables.
Q: What is the default if I omit -t, -c, or -d?
2 threads, 10 connections, and a 10-second duration.
Q: Why must connections be greater than or equal to threads?
Connections are divided evenly among threads, so each thread must own at least one connection. wrk enforces this and exits otherwise.
Q: Can I limit the request rate (throttle)?
Not with a flag. Use a script that defines delay() to return a per-request delay in milliseconds (see scripts/delay.lua).
Q: Can I get JSON or CSV output?
The built-in output is the fixed text report. For machine-readable output, define a done() function in a script and write whatever format you like (see scripts/report.lua).
Q: How do I send a POST with a body?
Set wrk.method, wrk.body, and a Content-Type header in a script (see scripts/post.lua). The Content-Length header is computed for you by wrk.format().
Q: Does wrk verify TLS certificates?
No. It connects to HTTPS endpoints without verifying the certificate. Do not rely on wrk to detect certificate problems.
Q: Does wrk support HTTP/2?
No. It speaks HTTP/1.x via the bundled HTTP parser.
Q: My script's counter resets between threads — is that a bug?
No. Each thread has its own independent Lua context, so a global like counter is per-thread. Use setup() with thread:set() to coordinate values across threads.
Q: What does the bracketed word in wrk --version mean (e.g. [epoll])?
It names the event-notification system the build uses: epoll on Linux, kqueue on macOS/BSD, evport on illumos/Solaris, or select as a fallback.
Q: How does wrk choose which addresses to use?
It resolves the host, then keeps only addresses it can actually connect to (wrk.resolve filters with wrk.connect). A script can override the per-thread address via thread.addr.
Q: How is Req/Sec different from Requests/sec?
Req/Sec is the per-thread rate sampled every 100 ms. Requests/sec is the overall throughput across all threads for the whole run.