# wrk Administrator & Developer Guide

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

```text
                         +--------------------------------------+
                         |              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_State`s 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

```bash
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:

```bash
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:

```c
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.

```bash
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:

```lua
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

```text
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
```

---

_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. Unofficial; not affiliated with or reviewed by the project._
