Jesse A. Green / notebook

wrk Administrator & Developer Guide

Generated June 2026 from wg/wrk at a211dd5

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the architectural and operational reference for wrk, the multithreaded HTTP benchmarking tool. It is written for the people who deploy wrk in test infrastructure, build it for distribution, integrate it into pipelines, and read or extend its source. Everything here is derived directly from the source at commit a211dd5.

For installation and day-to-day usage, see the companion User Guide. This document concentrates on how wrk is built, how its internals fit together, what its security posture is, and how to operate it reliably.


1. Executive Summary

wrk is a single-process, multithreaded HTTP/1.x load generator written in C99 with an embedded LuaJIT scripting engine. It is designed to saturate a target server from one multi-core machine by pairing a small pool of OS threads with a high-performance, OS-native event loop (epoll, kqueue, event ports, or select).

Attribute Value
Language C (C99) plus LuaJIT for scripting
Concurrency model N OS threads, each running an independent event loop and Lua state
I/O model Non-blocking sockets driven by an event-notification API
Protocol HTTP/1.x over plain TCP or TLS (OpenSSL)
Configuration surface Command-line flags + optional Lua script only — no config files, no environment variables
External dependencies LuaJIT, OpenSSL (both bundled and built statically by default)
Platforms Linux, macOS, FreeBSD, illumos/Solaris (UNIX-like)
Build system GNU make
Distinguishing feature Coordinated-omission correction for honest tail-latency numbers

The design philosophy is minimalism: a small, auditable core that does one thing — generate load and measure latency accurately — and delegates customization to Lua rather than accreting flags. A large fraction of the C source is well-known vendored code (the Redis event loop, the Node.js HTTP parser, Redis's zmalloc), leaving the project's own logic compact and focused.


2. Architecture

2.1 High-level structure

                         +--------------------------------------+
                         |              main() / wrk.c          |
                         |  parse_args -> resolve -> spawn       |
                         |  threads -> sleep(duration) -> join   |
                         |  -> aggregate -> print -> done()      |
                         +-------------------+------------------+
                                             |
              pthread_create per thread      |  shared config (cfg), statistics
              +------------------------------+------------------------------+
              |                              |                              |
        +-----v------+                 +-----v------+                 +-----v------+
        |  thread 0  |                 |  thread 1  |       ...       |  thread N  |
        |  ae loop   |                 |  ae loop   |                 |  ae loop   |
        |  Lua state |                 |  Lua state |                 |  Lua state |
        | C conns/T  |                 | C conns/T  |                 | C conns/T  |
        +-----+------+                 +-----+------+                 +-----+------+
              |                              |                              |
      non-blocking sockets           non-blocking sockets           non-blocking sockets
      (net.c / ssl.c)                via sock function table         TCP_NODELAY, keep-alive
              |                              |                              |
              +------------------------------+------------------------------+
                                             |
                                   +---------v---------+
                                   |   target server   |
                                   +-------------------+

The main thread parses arguments, resolves the target, creates one worker thread per -t, sleeps for the test duration, then signals every thread to stop, joins them, aggregates their counters, and prints the report.

2.2 Threading and connection model

  • main() creates cfg.threads worker threads with pthread_create, each running thread_main.
  • Each thread owns its own aeEventLoop, its own LuaJIT lua_State, and connections / threads connection structures.
  • The event loop is sized at 10 + connections * 3 file-descriptor slots per thread to leave headroom for reconnection churn.
  • Threads are entirely independent during the run — there is no cross-thread shared mutable request state — which is why a script's globals are per-thread.
  • The duration is enforced by the main thread calling sleep(cfg.duration) and then setting a global stop flag; each thread's periodic timer observes the flag and calls aeStop on its loop.

2.3 Per-thread event loop

Each connection is a non-blocking socket registered with the thread's event loop. The lifecycle is a state machine driven by readability/writability callbacks in wrk.c:

Callback Trigger Responsibility
connect_socket thread start / reconnect Create the socket, set O_NONBLOCK and TCP_NODELAY, begin async connect
socket_connected socket becomes writable after connect Finish TLS handshake (if any), init the HTTP parser, register read+write handlers
socket_writeable socket writable Optionally apply scripted delay(), (re)build the request for dynamic scripts, write it, track partial writes
socket_readable socket readable Read into an 8 KB buffer (RECVBUF), feed bytes to the HTTP parser, count bytes
response_complete parser finishes a response Record latency, count status errors, invoke response() script hook, decide keep-alive vs reconnect
record_rate every 100 ms timer Sample the thread's request rate into the requests histogram; honor the stop flag

Note

When the server sends Connection: close (or HTTP keep-alive is otherwise not indicated), wrk tears down and reconnects the socket (reconnect_socket) rather than reusing it. Persistent connections are far more efficient; design your test target to support keep-alive for realistic throughput.

2.4 The socket abstraction (plain vs TLS)

net.c and ssl.c implement the same five-function interface, dispatched through a struct sock function table (connect, close, read, write, readable). At startup main() inspects the URL scheme: for https it swaps the plain sock_* functions for the ssl_* versions. The rest of the engine is oblivious to which transport is in use — a clean strategy-pattern boundary.

Both transports return a three-valued status (OK, ERROR, RETRY); RETRY lets the non-blocking loop re-arm on EAGAIN / SSL_ERROR_WANT_* without treating a would-block as a failure.

2.5 Statistics engine

stats.c implements a fixed-bucket histogram (stats struct: a flat uint64_t data[] array indexed by value). This gives O(1) recording and bounded memory.

  • Latency is recorded in microseconds, bucketed up to timeout * 1000.
  • Request rate is recorded into a histogram sized to MAX_THREAD_RATE_S (10,000,000).
  • Recording uses lock-free atomics (__sync_fetch_and_add, compare-and-swap on min/max), so threads update shared histograms without a mutex.
  • Derived metrics — stats_mean, stats_stdev, stats_percentile, stats_within_stdev — are computed once at the end.

Coordinated-omission correction

After the run, stats_correct adjusts the latency histogram against the expected per-connection request interval. If the server stalls, a naive load generator stops issuing requests and therefore never records the latency those would-be requests experienced — hiding the true tail. wrk back-fills synthetic samples so a long pause is reflected across the percentiles. This is the single most important correctness feature of wrk's measurement and the reason its p99 numbers are trustworthy.

2.6 Unit parsing and formatting

units.c provides the SI (1k/1M/1G) and time (s/m/h) parsing used for arguments (scan_metric, scan_time) and the human-friendly formatting of the report (format_metric, format_binary, format_time_us). Binary sizes use 1024 scaling; metric counts use 1000 scaling.

2.7 Component inventory

File Origin Role
wrk.c project Entry point, argument parsing, thread orchestration, socket state machine, reporting
wrk.h project Core types: thread, connection, buffer; tunable constants
script.c / script.h project LuaJIT bridge: Lua state setup, hook invocation, wrk/thread/stats/addr metatables
wrk.lua project Lua-side glue: the wrk table, wrk.format, wrk.resolve, wrk.init, wrk.setup
net.c / net.h project Plain-TCP socket operations and the sock/status interface
ssl.c / ssl.h project OpenSSL transport implementing the same socket interface
stats.c / stats.h project Histogram statistics, percentiles, coordinated-omission correction
units.c / units.h project SI/time argument parsing and report formatting
aprintf.c / aprintf.h project Allocating printf helper used by formatting
ae.c, ae_epoll.c, ae_kqueue.c, ae_evport.c, ae_select.c, ae.h Redis (Salvatore Sanfilippo) The event loop and its per-OS backends
http_parser.c / http_parser.h Joyent / Node.js The HTTP/1.x response parser
zmalloc.c / zmalloc.h, atomicvar.h Redis Allocation wrappers and atomic-counter macros
config.h project Compile-time selection of the event backend per OS

Important

Roughly two-thirds of the C source is vendored, audited code from Redis and Node.js. When reasoning about wrk's behavior or attributing licenses, consult the NOTICE file. The project's own surface is small: wrk.c, script.c, net.c, ssl.c, stats.c, units.c, aprintf.c, and wrk.lua.


3. The LuaJIT integration

The C–Lua boundary lives in script.c. Understanding it matters for anyone writing non-trivial scripts or debugging script behavior.

3.1 State creation

script_create builds a fresh lua_State per thread: it opens the standard libraries, loads the embedded wrk module (require "wrk"), registers three metatables (wrk.addr, wrk.stats, wrk.thread), parses the URL, and populates wrk.scheme/host/port/path plus the headers table from the -H flags. The wrk.lua source is compiled to bytecode at build time and linked into the binary, so no .lua file needs to ship alongside wrk.

3.2 Static vs dynamic requests

At startup the first thread's state is interrogated to set engine-wide behavior flags:

Flag Set by Effect
cfg.pipeline script_verify_request Number of HTTP requests found in the generated request buffer (enables pipelining)
cfg.dynamic !script_is_static Whether request() is a function (rebuild every cycle) vs a static buffer (build once)
cfg.delay script_has_delay Whether a delay() hook exists
response parsing script_want_response Header/body parser callbacks are only wired up if a response() hook exists

This lazy wiring is a deliberate performance optimization: if your script does not define response(), wrk never pays to parse headers and bodies.

3.3 Request verification

Before load begins, script_verify_request parses the first generated request through the HTTP parser. A malformed request causes wrk to print <message> at <line>:<column> (or incomplete request) and exit(1). This fail-fast check prevents a whole run from silently producing garbage.

3.4 Cross-thread data transfer

script_copy_value deep-copies values between lua_States for thread:get/thread:set. Only boolean, nil, number, string, and tables of those types are transferable; anything else raises a Lua error. This is the only sanctioned channel for moving data between the setup context and a running thread.


4. Building & Packaging

4.1 The default build

make

The default target compiles the project sources plus bundled LuaJIT and OpenSSL into a single statically linked wrk binary. Key build settings from the Makefile:

Setting Value
CFLAGS -std=c99 -Wall -O2 -D_REENTRANT (plus per-OS additions)
LIBS -lluajit-5.1 -lm -lssl -lcrypto -lpthread (plus -ldl on Linux)
Object dir obj/
Version string git describe --tags --always --dirty, compiled into obj/version.o
Lua bytecode src/wrk.lua compiled via luajit -b into obj/bytecode.c

Per-OS adjustments are applied automatically based on uname -s:

OS (TARGET) Extra flags
linux -D_POSIX_C_SOURCE=200112L -D_BSD_SOURCE -D_DEFAULT_SOURCE, -ldl, -Wl,-E
darwin sets MACOSX_DEPLOYMENT_TARGET
freebsd -D_DECLARE_C99_LDBL_MATH, -Wl,-E
sunos -D_PTHREADS -D_POSIX_C_SOURCE=200112L, -lsocket

4.2 Building against system libraries

For distribution packaging, link the system LuaJIT and OpenSSL instead of the bundled copies:

make WITH_LUAJIT=/usr WITH_OPENSSL=/usr

Each variable adds the corresponding include and lib directories to the compile and link flags and skips building that bundled dependency.

Caution

System OpenSSL must be 1.1.0 or newer (per the changelog). Linking an older OpenSSL will fail to build or misbehave at runtime.

4.3 The compile-time event backend

config.h selects the event-notification backend at compile time from the platform macros — kqueue on FreeBSD/macOS, epoll on Linux, event ports on illumos/Solaris — falling back to select. There is no runtime switch; the backend is baked into the binary and reported by wrk --version.

4.4 Deployment checklist

  • [ ] Build on, or for, the same OS/architecture as the load-generating host.
  • [ ] Confirm OpenSSL ≥ 1.1.0 if using WITH_OPENSSL.
  • [ ] Run wrk --version and confirm the expected event backend (epoll/kqueue/etc.).
  • [ ] Smoke-test against a known endpoint before a real campaign.
  • [ ] Copy the single wrk binary to a directory on PATH.

5. Security Analysis

wrk is a client-side load generator; its threat surface is small but has a few sharp edges that operators must understand.

5.1 TLS certificate verification is disabled

In ssl_init, the TLS context is created with:

SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
SSL_CTX_set_verify_depth(ctx, 0);

wrk performs no certificate validation. It will happily connect to hosts with expired, self-signed, mismatched, or otherwise untrusted certificates.

Warning

Because certificate verification is off and cannot be enabled by any flag, wrk must never be used to assert that a server's TLS is correctly configured, and it is unsuitable as a connectivity/security probe. It is a benchmarking tool only. Treat any HTTPS benchmark as transport-encrypted but unauthenticated.

The context does send SNI (SSL_set_tlsext_host_name) and uses SSLv23_client_method (negotiate the best available protocol), with client-side session caching enabled.

5.2 Lua scripts execute with full privileges

A -s script runs with the standard Lua libraries opened (luaL_openlibs), including io and os. A script can read and write files and run commands with the privileges of the user running wrk.

Caution

Treat -s scripts as trusted code. Never run a wrk script from an untrusted source, and never run wrk as root with a third-party script. There is no sandbox.

5.3 No authentication handling beyond headers

wrk has no notion of credentials, cookies, or auth flows. Anything sensitive (bearer tokens, API keys) is passed via -H headers or set in a script, and will appear in process arguments / script files. Protect those as you would any secret on the host.

5.4 Denial-of-service potential

wrk is, by design, a tool for generating overwhelming load. Pointed at a third party it is an effective DoS instrument.

Important

Only benchmark systems you own or are explicitly authorized to test. High connection/thread counts against an unprepared target can exhaust its sockets, file descriptors, and CPU.

5.5 Input handling

wrk parses only the URL and the responses it receives. Response parsing uses the hardened, widely deployed Node.js HTTP parser. Certificate verification aside, there is no parsing of untrusted local input beyond the operator-supplied script.


6. Operations

6.1 Running a campaign

There are no services, daemons, or background state. Each invocation is a self-contained run that prints a report and exits. Operational control is therefore entirely about invocation parameters and host tuning.

wrk -t12 -c400 -d2m --latency --timeout 5s https://target.internal/health

6.2 Host tuning for high load

The load-generating host, not wrk, is usually the bottleneck. Tune it:

Concern Tuning
Ephemeral ports Widen the local port range (net.ipv4.ip_local_port_range) so high -c does not exhaust ports
Socket reuse Ensure closed sockets recycle quickly (net.ipv4.tcp_tw_reuse where appropriate)
File descriptors Raise ulimit -n above the connection count plus headroom
Threads Keep -t near the core count; oversubscription adds scheduling overhead

On the target side, raise the listen(2) backlog above the concurrent connection count so the initial connection burst is absorbed rather than refused.

6.3 Stopping a run

Send SIGINT (Ctrl+C). wrk installs a handler that sets the stop flag; threads finish their current cycle, the loops stop, and the partial report prints. SIGPIPE is ignored so a peer closing mid-write does not kill the process.

6.4 Interpreting results operationally

  • Compare Requests/sec across runs, not absolute numbers across tools — methodology differs.
  • Watch the error lines. Non-2xx or 3xx responses and Socket errors invalidate a throughput number if high; the server may be shedding load.
  • Trust p99 over the average. Use --latency and read the 99th percentile for tail behavior; thanks to coordinated-omission correction these numbers reflect real stalls.
  • Re-run for stability. A low +/- Stdev indicates consistent timing; a high one means the result is noisy and should be repeated.

6.5 Integrating into CI/automation

Because output is fixed-format text, parse it with a done() script that emits machine-readable output instead of scraping stdout:

done = function(summary, latency, requests)
   io.write(string.format("p99_us=%d rps=%.0f errors=%d\n",
      latency:percentile(99),
      summary.requests / (summary.duration / 1e6),
      summary.errors.status + summary.errors.timeout))
end

Tip

wrk has no non-zero exit code for "benchmark failed" conditions like high error rates — it exits 0 after a normal run. If you gate a pipeline on results, compute pass/fail in your done() script and emit a sentinel your harness can check.


7. Tunable Constants & Internals Reference

Compile-time constants from wrk.h that govern runtime behavior:

Constant Value Meaning
RECVBUF 8192 Per-read socket buffer size (bytes)
SOCKET_TIMEOUT_MS 2000 Default request timeout (overridable with --timeout)
RECORD_INTERVAL_MS 100 How often each thread samples its request rate
MAX_THREAD_RATE_S 10000000 Upper bound of the request-rate histogram

Default configuration applied in parse_args (before flags):

Field Default
threads 2
connections 10
duration 10 (seconds)
timeout SOCKET_TIMEOUT_MS (2000 ms)

Validation rules enforced by parse_args:

  • A URL argument is required, with a parseable scheme and host.
  • threads and duration must be non-zero.
  • connections must be non-zero and >= threads.

Negative knowledge (what wrk does NOT have)

These are common assumptions that are false for wrk. Documented to prevent fabrication.

  • No configuration file of any kind, and no environment variables are read. Configuration is CLI + script only.
  • No -r option. The character r appears in the getopt option string but has no handler; passing -r falls through to the usage error. It is vestigial and non-functional.
  • No built-in request-rate limiting flag. Throttling is only possible via a delay() script hook.
  • No JSON/CSV output flag. Machine-readable output requires a done() script.
  • No HTTP/2 or HTTP/3. The bundled parser handles HTTP/1.x only.
  • No cookie jar or redirect following. Responses are measured, not navigated.
  • No certificate verification, and no flag to enable it.
  • No distributed/cluster mode. One process, one host. Coordinate multiple hosts externally if you need more load than one machine can produce.
  • No Windows support.
  • No done()-driven exit codes. A completed run exits 0 regardless of error counts.

8. Potential Improvements

Observations for maintainers and contributors, grounded in the current source:

  • Optional certificate verification. A flag to enable SSL_VERIFY_PEER (with a CA path) would let wrk double as a realistic HTTPS client without weakening the default benchmarking behavior.
  • Configurable percentile set. The --latency distribution is hard-coded to 50/75/90/99 (print_stats_latency). Exposing the percentiles (or always including 99.9/99.99) would aid tail analysis without a custom done() script.
  • A machine-readable output mode. A built-in --json would remove the need for a boilerplate done() script in CI and standardize parsing.
  • Non-zero exit on error thresholds. An optional flag to exit non-zero when Non-2xx/3xx or socket errors exceed a bound would make pipeline gating ergonomic.
  • Connection distribution remainder. connections / threads integer division silently drops the remainder; documenting or redistributing it would avoid surprising users who pass non-multiple counts.
  • Remove the vestigial -r token from the getopt string to avoid confusion.

Note

wrk's value lies in its small, auditable core and accurate measurement. Improvements should preserve that minimalism — most customization belongs in Lua, not new flags.


9. Quick Reference

Command-line options

Short Long Arg Default
-c --connections N 10
-d --duration T 10s
-t --threads N 2
-s --script file —
-H --header header —
--latency — off
--timeout T 2s
-v --version — —
-h --help — —

Lua hooks

Hook Signature Phase
setup setup(thread) setup
init init(args) running
delay delay() -> ms running
request request() -> string running
response response(status, headers, body) running
done done(summary, latency, requests) done

wrk table & helpers

Member Purpose
wrk.scheme/host/port/method/path/headers/body Request template fields
wrk.format(method, path, headers, body) Build a request string
wrk.lookup(host, service) Resolve to a table of addresses
wrk.connect(addr) Test connectivity to an address
thread.addr / thread:get/set/stop Per-thread control

Stats object

Accessor Returns
.min / .max / .mean / .stdev Summary values
:percentile(p) Value at percentile p
(i) Raw value and count at index i
#stats Number of distinct recorded values

Source tree

wrk/
├── Makefile          # build, dependency bundling, per-OS flags
├── INSTALL           # build instructions
├── SCRIPTING         # Lua API reference
├── CHANGES           # changelog
├── NOTICE            # vendored-code attributions & licenses
├── scripts/          # example Lua scripts
│   ├── addr.lua      # per-thread address selection
│   ├── auth.lua      # capture token from a response
│   ├── counter.lua   # dynamic path/header per request
│   ├── delay.lua     # random per-request delay
│   ├── pipeline.lua  # HTTP pipelining
│   ├── post.lua      # POST with body
│   ├── report.lua    # custom CSV report via done()
│   ├── setup.lua     # cross-thread data via setup()
│   └── stop.lua      # thread:stop() after N responses
└── src/
    ├── wrk.c/.h      # entry point, orchestration, socket state machine
    ├── wrk.lua       # Lua-side glue (wrk table, format, resolve)
    ├── script.c/.h   # LuaJIT bridge
    ├── net.c/.h      # plain TCP transport
    ├── ssl.c/.h      # OpenSSL transport
    ├── stats.c/.h    # histogram + coordinated-omission correction
    ├── units.c/.h    # SI/time parsing & formatting
    ├── aprintf.c/.h  # allocating sprintf
    ├── ae*.c, ae.h   # Redis event loop (epoll/kqueue/evport/select)
    ├── http_parser.* # Node.js HTTP/1.x parser
    ├── zmalloc.*     # Redis allocation wrappers
    ├── atomicvar.h   # atomic-counter macros
    └── config.h      # compile-time backend selection