Hold the rate.
Expose the tail.
zrk is a constant/linear-throughput HTTP(S) load generator — a Zig rewrite of wrk2 that paces in nanoseconds, not milliseconds. It clocks each request from the moment it should have been sent, so a server stall lands in the histogram instead of vanishing from it — and the pacing itself never becomes part of what you're measuring.
zrk’s live dashboard: elapsed time, offered and achieved rates, transfer, and status counts, then a percentile readout from p50 to max, then a live latency spectrogram — a heat-shaded waterfall of the latency distribution for each one-second interval, newest at the bottom, with a latency scale below it — redrawn every second while a simulated sixty-second ramp runs from 100 to 5,000 requests per second. A 120 millisecond stall at 22 seconds lights up a second, higher band in the waterfall for one interval; past the origin’s capacity near 4,300 requests per second the achieved rate falls behind the schedule and turns red while a second band splits off from the first and climbs higher each second, a queue of backlogged requests the bars this panel replaced would have blended into one stretched-out tail. When the run ends the panel settles in place — the recording dot stops blinking — rather than switching to a separate report.
One static binary — a ~615 KB download, ~1.4 MB unpacked.
Zig 0.16 · HTTP/1.1 · MIT · one fixed request per run
Coordinated omission
The stall you don’t measure is still there.
Most load tools are closed loops: send, wait for the answer, send again. When the server stalls, the tester politely stops sending — the stall produces almost no samples, and the report comes back clean. Gil Tene named the bug coordinated omission; wrk2 was the fix. zrk carries it forward and paces to the nanosecond instead of wrk2's millisecond tick, so every backlogged request lands exactly where it should and gets measured, not smoothed away.
HdrHistogram
Every request, not five percentiles.
An HdrHistogram buckets latency exponentially, with linear sub-buckets inside each decade — so a 200 µs request and a 200 ms request both land at the same three significant figures of precision. Fixed-width buckets can't do that: fine enough for the baseline, they'd need millions of buckets for the tail; coarse enough for the tail, they'd blur the very stall coordinated omission exists to catch. Every corrected latency zrk records lands here, and the shape survives whole — merge histograms across connections or intervals and get the exact distribution back, not an average of averages.
Surface
One binary, no scripts.
The whole tool is a single static binary — a histogram, a pacer, an HTTP codec, a dashboard. Everything it can do is a flag; these six are why it exists.
-R 2000 · -R 100:5000A rate is a promise
zrk offers load; it does not ask permission. The target rate splits evenly across connections and each one paces its own schedule. A:B ramps the rate linearly over the run — sweep it past the knee and you get the latency-vs-load curve, not one blended number.
--latency · --hdr out.hgrmThe whole distribution
Every corrected latency lands in an HdrHistogram — 1 µs to 60 s at three significant figures, with anything past the ceiling clamped into the top bucket rather than dropped.
--hdrwrites the classic percentile file the HdrHistogram plotter loads directly, format-compatible with wrk2’s--latencyoutput.--timeseries ramp.ndjsonOne row per interval
Each window streams its offered and achieved rate, counts, transfer, and percentiles as NDJSON. Add
--timeseries-histogramand every row carries the window’s full distribution — re-percentile a single interval, or merge any subset losslessly.--slo-p99 250ms · --max-error-rate 1%A benchmark that can fail the build
SLO gates exit with code 3 on breach and
--formatjson emits one parseable summary. The dashboard falls back to append-only lines when stdout is not a TTY, so piping into CI just works.--deadline 250ms · --deadline-abortBound the tail, not just the wire
--timeoutis a wire clock — measured from the actual send, so under overload a connection drifts a whole schedule behind while every attempt still returns fast, and it never bounds CO latency.--deadlineworks from the scheduled send by shedding before sending: a too-stale request is failed as a deadline error without ever touching the wire, so the backlog drains as errors and no connection is reset — a request that does go out then runs to completion, so recorded latency is bounded by deadline + wire time and--slo-p99stays a p99 of met latency.--deadline-abortopts into cutting an in-flight request off at the deadline exactly, at the cost of a connection reset per miss that storms a saturated target — off by default. Alongside, a peak-lag gauge (max_schedule_lag_us) flags the moment the client falls behind its own schedule.-c 200 · -t 4Threads for parallelism, not per-connection
Every connection runs as a zio coroutine with one request in flight;
-tsets how many OS threads the runtime schedules them across (default 2, real parallelism — not cooperative green threads), and-csets how many connections run. Growing-tbuys throughput headroom, not more open connections. Each connection still owns a lock-free histogram and publishes a snapshot per interval.
Reporting
Built to be parsed.
--format json emits one summary object; --timeseries streams a
row per interval; --hdr writes the classic .hgrm file the HdrHistogram plotter loads directly. Distributions travel whole — not as five lossy percentiles.
{ "target": { "url": "http://origin:8080/", "method": "GET" }, "requests": 19998, "achieved_rate": 999.80, "rate_ratio": 0.9998, "error_rate": 0.000000, "max_schedule_lag_us": 84, "latency_us": { "min": 106, "mean": 422.0, "max": 13991, "p50": 251, "p75": 337, "p90": 471, "p99": 1913, "p99_9": 13364, "p99_99": 13988 }, "status_codes": { "2xx": 19998, "5xx": 0 }, "latency_histogram": "HISTFAAAAUJ4nC1P…"}{"t":1.004,"target_rate":181.7,"achieved_rate":180.9,"requests":181,"errors":0,"bytes":11222,"bytes_per_sec":11177.4,"latency_us":{"p50":244,"p90":651,"p99":1699,"p99_9":2210,"max":2210}}{"t":2.001,"target_rate":263.3,"achieved_rate":262.8,"requests":263,"errors":0,"bytes":16306,"bytes_per_sec":16289.7,"latency_us":{"p50":247,"p90":668,"p99":1745,"p99_9":2388,"max":2388}}{"t":3.006,"target_rate":345.0,"achieved_rate":344.1,"requests":344,"errors":0,"bytes":21328,"bytes_per_sec":21285.4,"latency_us":{"p50":251,"p90":673,"p99":1821,"p99_9":2456,"max":2456}}- latency_histogram is the complete distribution as an HdrHistogram V2 compressed blob — the interchange format the Java/Go/JS/Rust libraries read. Store the blob, re-percentile a run after the fact, diff two runs, merge a hundred.
- achieved_rate and rate_ratio certify that the offered load was actually delivered — if they sag, the numbers describe your client, not your server. max_schedule_lag_us is the companion gauge: the peak the client ever fell behind its own schedule, an early warning that surfaces before the ratio does.
- The final summary blends a whole ramp into one histogram; the time series is the artifact a ramp exists to produce. Merging every row reproduces the summary exactly, since the intervals partition the run.
The instrument behind the bench.
Every number on the zoxy benchmark was offered at a fixed schedule and corrected for coordinated omission by zrk — the same ramp, paced to the nanosecond, for all six proxies. At the sub-millisecond gaps that separate a fast proxy from a faster one, a coarser scheduler’s own noise would have been the whole signal. Read the full report, or point zrk at your own service and find its knee.