# zrk **Repository Path**: mirrors_floatdrop/zrk ## Basic Information - **Project Name**: zrk - **Description**: HTTP benchmarking tool in Zig based mostly on wrk2 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-10-25 - **Last Updated**: 2026-10-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

zrk ramping load from 400 to 3400 req/s against a saturating service

# zrk [![CI](https://github.com/zoxy-io/zrk/actions/workflows/ci.yml/badge.svg)](https://github.com/zoxy-io/zrk/actions/workflows/ci.yml) A constant-throughput HTTP load generator — a Zig 0.16 rewrite of [wrk2](https://github.com/giltene/wrk2) with a live in-terminal dashboard. - **Corrected for coordinated omission.** Latency is measured from the time a request *should* have been sent, so a server stall lands in the tail instead of being smoothed away. - **Nanosecond pacing.** The send schedule is a closed-form nanosecond offset, not wrk2's millisecond timer wheel — which rounds every wait up and adds ~0.5 ms of the tool's own noise to every sample. ([why this matters](docs/coordinated-omission.md#why-zrk-is-more-accurate-than-wrk2)) - **Three load models.** Fixed rate (`-R 2000`), linear ramp (`-R 100:5000`), or closed loop (`--closed`) to discover a ceiling instead of guessing one. - **Live dashboard.** Latency percentile spectrum and a p99 sparkline while the run is going; falls back to append-only lines when stdout is not a TTY. - **Machine-readable output.** JSON summary, HdrHistogram (`.hgrm` and V2 base64), and per-interval NDJSON for streaming plotters. - **CI gates.** `--slo-p99` and `--max-error-rate` fail the build with exit 3. ## Installation ### Homebrew ```sh brew install zoxy-io/tap/zrk ``` The tap follows stable releases only. A prerelease — `v3.0.0-alpha.1` and anything else with a `-` in its version — is published to [Releases](https://github.com/zoxy-io/zrk/releases) as archives and is deliberately not written to the tap, because the tap holds one formula and `brew upgrade` reads it. Take a prerelease from the download below. ### Pre-built binary Download the latest release binary from the [Releases page](https://github.com/zoxy-io/zrk/releases). Linux and macOS, x86_64 and aarch64. Windows binaries were dropped after v1.4.3 ([#21](https://github.com/zoxy-io/zrk/issues/21)): zrk terminates TLS through [zssl](https://github.com/zoxy-io/zssl), which is Linux/macOS by design. ### Build from source Requires Zig 0.16. ```sh zig build # produces zig-out/bin/zrk zig build -Doptimize=ReleaseFast zig build test # run the unit + integration tests ``` ## Usage ``` zrk — constant-throughput HTTP load generator Usage: zrk [options] Options: -t, --threads Total number of threads to execute load (default 2) -c, --connections Total connections to keep open (default 10) -s, --streams HTTP/2 or HTTP/3 streams in flight per connection (default 1). A depth knob only: -R still splits across -c, so -c 10 -s 10 offers the same rate as -c 10, not as -c 100. Requires --http2 or --http3 -d, --duration Test duration, e.g. 30s, 2m (default 10s) -R, --rate Target requests/second (total); A:B ramps linearly from A to B over the run (default 1000) --closed Closed-loop mode: ignore -R, send each connection's next request the instant its previous response completes (like wrk/ab). No coordinated-omission correction; the rate finds its own ceiling instead of chasing one. Incompatible with a ramp (-R A:B) or --deadline --disable-keepalive Close and reconnect after every response: one connection per request, like ab. Enforced client-side, so it also covers servers that ignore the Connection: close it sends. Not available with --http2 or --http3 -H, --header Add a request header (repeatable) -m, --method HTTP method (default GET) -b, --body Request body; @FILE reads it from a file (@- = stdin, @@x = a literal "@x") --timeout Wire timeout per attempt, from the actual send (default 2s); does not bound CO latency --deadline Max coordinated-omission latency, from the scheduled send: a too-stale request is shed (failed as a `deadline` error, not sent or recorded) before sending (0 = off) --deadline-abort Also abort in-flight requests past the deadline. Resets the connection per miss and churns under saturation; off by default --interval Stats window: --timeseries rows and --plain lines (default 1s) --refresh Live dashboard redraw rate (default 80ms) --latency Print full latency spectrum in the final report --http2 Speak HTTP/2. Cleartext uses prior knowledge (h2c); https negotiates it over ALPN and fails the connection if the server declines --http3 Speak HTTP/3 over QUIC (https only) -k, --insecure Skip TLS certificate verification --plain Append-only output instead of a live dashboard Reporting: --format Final report format (default text) -o, --output Write the final report to FILE (default stdout) --hdr Also write the HdrHistogram percentile distribution (.hgrm) to FILE --timeseries Stream per-interval NDJSON (throughput + latency percentiles) to FILE. "-" streams to stdout for piping into a live plotter; the dashboard is then suppressed and the final report goes to stderr unless -o is given --timeseries-histogram Add each interval's full latency histogram (HdrHistogram base64) to every --timeseries row --no-record-timeouts Drop wire-timed-out requests from the latency histogram (default: record them). Independent of --deadline misses, which are never recorded. CI gates (exit code 3 on breach): --slo-p99 Fail if final p99 latency exceeds T --max-error-rate Fail if error rate exceeds F (0..1) -h, --help Show this help --version Show version ``` Durations accept `us`, `ms`, `s`, `m`, `h` (a bare number is seconds). Short options may be attached (`-c100`) or separated (`-c 100`). ### Examples ```sh # 2000 req/s for 30s over 100 connections zrk -c100 -d30s -R2000 http://127.0.0.1:8080/ # Ramp linearly from 100 to 5000 req/s over 60s, capturing the latency-vs-load # curve as a per-interval NDJSON time series (find the knee where latency breaks) zrk -c200 -d60s -R100:5000 --timeseries ramp.ndjson http://127.0.0.1:8080/ # Closed-loop: what's the real max throughput at 100 connections? No -R to # guess — achieved_rate finds its own ceiling instead of chasing one. zrk -c100 -d20s --closed http://127.0.0.1:8080/ # HTTPS with the full latency spectrum in the final report zrk -c20 -d1m -R500 --latency https://api.example.com/health # POST with a body and custom headers (-b @payload.json reads a file, @- stdin) zrk -c10 -R100 -m POST -b '{"ping":1}' \ -H 'Content-Type: application/json' http://127.0.0.1:8080/echo # HTTP/3 over QUIC (experimental — see "HTTP/3" below for what that costs you) zrk --http3 -k -c10 -R500 -d30s https://127.0.0.1:4433/ # CI-friendly, no redrawing dashboard zrk -c50 -R1000 -d20s --plain http://127.0.0.1:8080/ | tee run.log # Machine-readable: JSON summary to a file + HdrHistogram .hgrm for plotting zrk -c50 -R1000 -d20s --format json -o result.json --hdr latency.hgrm \ http://127.0.0.1:8080/ # CI gate: fail the build (exit 3) if p99 regresses past 250ms or errors climb zrk -c50 -R1000 -d20s --format json -o result.json \ --slo-p99 250ms --max-error-rate 1% http://127.0.0.1:8080/ # Live terminal plot: stream the per-interval rows into jplot zrk -c50 -R1000 -d5m --timeseries - http://127.0.0.1:8080/ \ | jplot achieved_rate+target_rate latency_us.p50+latency_us.p90+latency_us.p99 error_rate ``` ### HTTP/3 `--http3` speaks HTTP/3 over QUIC through [h3](https://github.com/zoxy-io/h3), and is **experimental** ([#74](https://github.com/zoxy-io/zrk/issues/74)). It works, and the latency it reports means what every other transport's does — the coordinated-omission correction, `--deadline` shedding and the backlog gauge are the same code reading the same clock. Certificates are verified the same way too: `tls.Trust` builds the chain to a system anchor and matches the name for all three transports, so `-k/--insecure` is an opt-out here exactly as it is elsewhere. Three things are worth knowing before quoting a number from it: - **Large responses over a real network measure zrk, not the server.** Each stream's receive window is 16 KiB, so one stream moves at most 16 KiB per round trip. Over loopback that is invisible; at a 62 ms round trip a 126 KB page takes about 700 ms against HTTP/2's 110 ms, and a 1.3 MB one runs past the default `--timeout`. For responses beyond a few tens of KiB across a network, compare against `--http2` before trusting the figure; [#89](https://github.com/zoxy-io/zrk/issues/89) tracks raising it. - **A connection quiet for twice `--timeout` is replaced.** zrk sends no keepalive, and a QUIC server forgets an idle connection without a word, so a connection that has heard nothing for that long is not trusted with the next request. At a rate low enough to leave each connection idle that long — a large `-c` at a small `-R`, or the bottom of a ramp — requests carry a fresh handshake in their latency. - **One datagram per syscall on the way out.** Reads are batched — Linux generic receive offload collapses a burst into one read, worth about 40% against a server that segments — but sends are not. That was measured rather than assumed, and the measurement said not to bother: batching them is worth around 2% on a send-heavy workload and slightly negative on a receive-heavy one, because a QUIC client's egress is 60-to-70-octet packets and send syscalls are not what bounds this. [#83](https://github.com/zoxy-io/zrk/issues/83) revisits it for a path with real latency, which is the one case that could change the answer. Everything else carries over: `--closed`, ramps, `--timeseries`, the JSON summary and the CI gates all work unchanged. `--streams` works here as it does under `--http2`, and getting it there found a defect in h3: a multiplexed connection ran at full rate for about a second and then went to zero req/s, reporting no errors, because acknowledged packet contexts were truncated at thirty-two and the streams past that were never settled. It is fixed, and `build.zig.zon` pins the commit that fixes it — `src/h3conn.zig`'s module comment has the diagnosis. A `-c 16 -s 16 --closed` soak now runs 296,866 requests at 14.8k req/s where it previously managed 1,642 before stalling. ### Exit codes | code | meaning | |------|---------| | 0 | run completed; any configured gates passed | | 1 | the run failed to start or complete, or completed without a single successful request (see the message on stderr) | | 2 | bad arguments, or a `--body` file that could not be read | | 3 | run completed but a `--slo-p99` / `--max-error-rate` gate was breached | | 130 | interrupted by SIGINT (`Ctrl-C`); a partial report was still written | | 143 | interrupted by SIGTERM; a partial report was still written | ## Documentation | | | |---|---| | [Coordinated omission](docs/coordinated-omission.md) | Why the correction exists, when `--closed` is the right tool, and how zrk's clock differs from wrk2's. | | [HTTP/2 multiplexing](docs/multiplexing.md) | What `-c` and `-s` mean once a connection carries several requests, and why `-c 10 -s 10` is not `-c 100`. | | [`--timeout` vs `--deadline`](docs/deadlines.md) | Bounding the latency tail under overload, and the backlog gauge. | | [Machine-readable output](docs/output.md) | The JSON summary, `--hdr`, `--timeseries` NDJSON, and piping rows into a live plotter. | | [Interrupting a run](docs/signals.md) | What SIGINT/SIGTERM report, and what supervisors should know. | | [Library usage](docs/library.md) | Driving `runner.run` from Zig instead of the CLI. | | [How it works](docs/internals.md) | Concurrency model, histogram/memory budget, source layout. | ## License [MIT](LICENSE)