Examples
48 runnable examples
Every one is a complete, valid test you can run with loadr run <file>. They ship in the examples/ folder of every download, or grab them here.
The smallest useful loadr test: 10 virtual users hammering one endpoint for 30 seconds, with checks and a pass/fail threshold. loadr run examples/01-quickstart.yaml
Classic ramp test: climb to 50 VUs, hold, ramp down — with think time between requests so VUs behave like humans.
Open-model load: start exactly 100 iterations per second regardless of how slowly the target responds. Watch `dropped_iterations` to spot saturation.
Spike test with a ramping arrival rate: calm baseline, sudden 10x spike, recovery. The abort threshold kills the run if errors explode.
Data-driven login: each iteration takes the next row from users.csv (shared cursor across all VUs, wrapping at EOF).
Correlation: extract values from earlier responses (regex, CSS selector, JSONPath, headers, boundary) and reuse them in later requests.
Several scenarios running concurrently with independent executors, start times and tags, plus groups and a custom metric.
Full JavaScript lifecycle: setup() fetches an auth token once, every iteration runs an exported function, beforeRequest/afterRequest hooks fire around YAML requests, teardown() cleans up.
WebSocket load: connect, exchange messages, assert on the conversation.
gRPC load: compile .proto files in-process (no protoc needed) or use server reflection, then call unary and streaming methods dynamically.
GraphQL over HTTP with variables; GraphQL `errors` are detected and the request is marked failed when no data comes back.
Raw socket load: TCP request/response and UDP datagrams, with hex payloads.
One test, many environments: `loadr run -e staging examples/13-environments.yaml` deep-merges the env block over the file. Secrets come from the process environment and are never printed.
Long soak test wired into observability: Prometheus scrape endpoint, JSONL archive, and StatsD — plus an abort threshold as a circuit breaker.
Distributed run: submit this to a controller and the load is partitioned across every connected agent (VUs split, arrival rates divided, percentiles merged centrally from HDR histograms — never averaged). loadr controller --bind 0.0.0.0:7625 & loadr agent --join controller-host:7625 --name agent-1 & loadr run --controller controller-host:6464 examples/15-distributed.yaml
Flow control: repeat, while, if/else and weighted random branches — the control flow from Gatling (repeat/during/doIf/randomSwitch) and the weighted-task model from Locust, in declarative YAML.
Feeder strategies (Gatling: sequential / random / shuffle, plus JSON feeders) and a global request-rate ceiling (Gatling throttle / reachRps).
Server-Sent Events load: open an event stream, read until a condition, assert on the last event.
Real-browser load test: drive headless Chrome over CDP and capture Navigation Timing + Web Vitals (FCP, LCP, DCL, load) for a page. Unlike the protocol-level `http` client, the `browser` protocol renders the page in a real Chrome tab — running JavaScript, fetching subresources, and firing the load/DOMContentLoaded events — so the timings reflect what a user actually experiences. Each VU keeps its own warm tab for the whole run. Requires headless Chrome/Chromium installed on the runner; it is launched lazily on the first browser request.
Advanced HTTP defaults: the "power features" of the HTTP client, all in one test. Exercises: - cache: true per-VU browser-style HTTP cache (fresh hits + ETag/Last-Modified revalidation) - hosts: pin a hostname to a fixed address (curl --resolve) - discard_response_bodies drop bodies after reading them (high throughput) - tracing: true inject a W3C `traceparent` header per request - tls.min_version/max_version pin the negotiated TLS version to 1.2..1.3 Run it, optionally dumping every request/response to the console: loadr run examples/21-http-advanced.yaml loadr run examples/21-http-advanced.yaml --http-debug
Scenario lifecycle hooks + a custom summary report. Each VU logs in once via the scenario `on_start` hook, runs its `exec` function every iteration, then logs out once via `on_stop` when it retires. After the run, the exported handleSummary(data) function prints a custom end-of-run report instead of loadr's default console summary.
Tagged scenarios for selective runs. Each scenario carries `tags` (a map of name → value); `loadr run` filters scenarios by their tag *values*. loadr run examples/23-tags.yaml # all four scenarios loadr run --tags smoke examples/23-tags.yaml # only smoke scenarios loadr run --tags read,write examples/23-tags.yaml # read OR write (any-match) loadr run --tags full --exclude-tags write examples/23-tags.yaml # full, but not writes --tags keeps a scenario when it carries at least one of the listed values. --exclude-tags drops a scenario when it carries any of the listed values, and always wins over --tags.
Time-series report demo: a ramp-up / spike / ramp-down profile that produces an interesting shape over time. Run it, export the summary, then render the self-contained HTML report — every metric is charted against elapsed time (throughput, latency p50/p95/p99, active VUs, error rate), not just shown as an end-of-run average. loadr run --summary-export results.json examples/24-timeseries-report.yaml loadr report results.json -o report.html open report.html # hover the charts to read off any point in the run The summary JSON carries a top-level `timeline` array (one point per snapshot interval) that drives the charts; the aggregate tables remain exact.
Fused check-chains: extract → coerce type → transform → validate → save, all in one declarative `extract:` step (Gatling-style). Chains live happily next to the classic `type:`-tagged extractors — mix them freely. Each `chain:` is the variable name to save. Pick exactly one source (jmespath / jsonpath / regex / header / css / xpath / left+right boundary), then optionally `as:` a type, run a `transform:` pipeline, `check:` the value and fall back to a `default:`.
Deliberately produces a MIX of failures so the web UI's failure breakdown panel has something to group: a healthy endpoint, 4xx and 5xx responses, a check that fails, and a JS step that throws an uncaught exception. Run it with the management UI and watch the "Failure breakdown" panel: docker compose -f examples/harness/docker-compose.yml up -d --build loadr run --ui examples/26-failure-breakdown.yaml (go-httpbin's /status/{code} endpoint returns the requested status code.)
PostgreSQL load test, driven by the `loadr-plugin-postgres` native protocol plugin. PostgreSQL support is NOT built into loadr core — it is a runtime-loadable plugin that ships the heavy `sqlx` Rust driver on its own (with ONLY the `postgres` feature, so it carries no `rsa` advisory). The plugin declares the `postgres://`/`postgresql://` URL schemes, so once it is installed a request to one of those URLs routes straight to it; no explicit `protocol:` needed. Each request runs one configured query as the "request"; loadr records query latency in `postgres_req_duration`, the number of rows returned (SELECT) or affected (INSERT/UPDATE/DELETE) in `postgres_rows`, and any database error fails the request (status 1 = ok, status 0 = DB/transport error). Positional parameters are bound safely by the driver with `$1, $2, ...` placeholders. Build + install the plugin, then run: cargo build -p loadr-plugin-postgres --release mkdir -p dist && cp plugins/loadr-plugin-postgres/plugin.toml dist/ \ && cp target/release/libloadr_plugin_postgres.so dist/ loadr plugin install dist loadr run examples/27-postgres.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
MongoDB load test, driven by the `loadr-plugin-mongo` native protocol plugin. MongoDB support is NOT built into loadr core — it is a runtime-loadable plugin that ships the heavy `mongodb` Rust driver on its own. The plugin declares the `mongodb://` (and `mongo://`) URL scheme, so once it is installed a request to a `mongodb://` URL routes straight to it; no explicit `protocol:` is needed. Each request runs one operation (insert/find/update/delete/aggregate/command) from its `plugin:` block. loadr records the operation latency in `mongo_req_duration`, the count of documents inserted/matched/modified/returned in `mongo_docs`, and any driver error fails the request (status 0 = error, status 1 = ok). Build + install the plugin, then run: cargo build -p loadr-plugin-mongo --release mkdir -p dist && cp plugins/loadr-plugin-mongo/plugin.toml dist/ \ && cp target/release/libloadr_plugin_mongo.so dist/ loadr plugin install dist loadr run examples/28-mongo.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
MySQL load test, driven by the `loadr-plugin-mysql` native protocol plugin. MySQL support is NOT built into loadr core — it is a runtime-loadable plugin that ships the heavy `sqlx` Rust driver on its own (with ONLY the `mysql` feature). The plugin declares the `mysql://` URL scheme, so once it is installed a request to such a URL routes straight to it; no explicit `protocol:` is needed. Each request runs one configured query as the "request"; loadr records query latency in `mysql_req_duration`, the number of rows returned (SELECT) or affected (INSERT/UPDATE/DELETE) in `mysql_rows`, and any database error fails the request (status 1 = ok, status 0 = DB/transport error). Positional parameters are bound safely by the driver with `?` placeholders. Build + install the plugin, then run: cargo build -p loadr-plugin-mysql --release mkdir -p dist && cp plugins/loadr-plugin-mysql/plugin.toml dist/ \ && cp target/release/libloadr_plugin_mysql.so dist/ loadr plugin install dist loadr run examples/29-mysql.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
Redis (RESP) load test, driven by the `loadr-plugin-redis` native protocol plugin. Redis is NOT built into loadr core — it is a runtime-loadable plugin that speaks RESP directly over TCP (no `redis` crate, no OpenSSL). The plugin declares the `redis://` (and `rediss://`) URL scheme, so once it is installed a request to a `redis://` URL routes straight to it; no explicit `protocol:` is needed. Each request runs one command from its `plugin.command` array (argv form). loadr records the command latency in `redis_req_duration` and counts requests in `redis_reqs`. A successful reply is status 0; a RESP error reply is status 1 and fails the request. Build + install the plugin, then run: cargo build -p loadr-plugin-redis --release mkdir -p dist && cp plugins/loadr-plugin-redis/plugin.toml dist/ \ && cp target/release/libloadr_plugin_redis.so dist/ loadr plugin install dist loadr run examples/30-redis.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
Apache Kafka load test, driven by the `loadr-plugin-kafka` native protocol plugin. Kafka support is NOT built into loadr core — it is a runtime-loadable plugin that ships its own Kafka client. That client is `rskafka`, a PURE-RUST client (no librdkafka / C toolchain), so the plugin cross-compiles to every release target. The plugin declares the `kafka://` URL scheme, so once it is installed a request to a `kafka://` URL routes straight to it; no explicit `protocol:` is needed. The broker is the URL authority and the topic is the URL path (`kafka://broker:9092/topic`). Each request runs one operation (produce/fetch) from its `plugin:` block. loadr records the operation latency in `kafka_req_duration`, the count of messages produced/fetched in `kafka_msgs`, and any client error fails the request (status 1 = ok, status 0 = client/transport error). Build + install the plugin, then run: cargo build -p loadr-plugin-kafka --release mkdir -p dist && cp plugins/loadr-plugin-kafka/plugin.toml dist/ \ && cp target/release/libloadr_plugin_kafka.so dist/ loadr plugin install dist loadr run examples/31-kafka.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
RabbitMQ load test, driven by the `loadr-plugin-rabbitmq` native protocol plugin. RabbitMQ support is NOT built into loadr core — it is a runtime-loadable plugin that ships the pure-Rust `lapin` AMQP 0.9.1 client on its own. The plugin declares the `amqp://` / `amqps://` (and `rabbitmq://`) URL schemes, so once it is installed a request to one of those URLs routes straight to it; no explicit `protocol:` is needed. Each request runs one operation (publish/get) from its `plugin:` block. loadr records the operation latency in `rabbitmq_req_duration`, the count of messages published/consumed in `rabbitmq_msgs`, and any broker error fails the request (status 1 = ok, status 0 = AMQP/transport error). Build + install the plugin, then run: cargo build -p loadr-plugin-rabbitmq --release mkdir -p dist && cp plugins/loadr-plugin-rabbitmq/plugin.toml dist/ \ && cp target/release/libloadr_plugin_rabbitmq.so dist/ loadr plugin install dist loadr run examples/32-rabbitmq.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
Elasticsearch load test, driven by the `loadr-plugin-elasticsearch` native protocol plugin. Elasticsearch support is NOT built into loadr core — it is a runtime-loadable plugin. Elasticsearch's API is plain HTTP/JSON, so the plugin talks to it over loadr's own hyper + hyper-rustls stack (pure-Rust TLS, no system OpenSSL); it does not pull in the heavy official `elasticsearch` crate. The plugin declares the `elasticsearch://` (and `es://`) URL schemes — both mapped onto `http://` internally — so once it is installed a request to one of those URLs routes straight to it; no explicit `protocol:` is needed. A plain `http(s)://` URL works too. Each request runs one operation (index/get/search/bulk) from its `plugin:` block. loadr records the operation latency in `elasticsearch_req_duration`, the count of documents written (index = 1, bulk = items succeeded) in `elasticsearch_docs`, and any HTTP/cluster error fails the request (status 1 = ok, status 0 = error). Build + install the plugin, then run: cargo build -p loadr-plugin-elasticsearch --release mkdir -p dist && cp plugins/loadr-plugin-elasticsearch/plugin.toml dist/ \ && cp target/release/libloadr_plugin_elasticsearch.so dist/ loadr plugin install dist loadr run examples/33-elasticsearch.yaml Or point the plan's `plugins:` entry at the built artifact directly (below).
A protocol plugin written in plain C, loaded over loadr's frozen C ABI. Unlike the Rust native plugins (mongo/redis/...), `c-echo` is a C shared library exporting four `extern "C"` symbols — proving loadr plugins can be authored in non-Rust languages. The host auto-detects the C ABI at load time. It serves the `cecho://` scheme and echoes each request body back, status 200. Build + install the plugin, then run: make -C examples/plugins/c-echo mkdir -p dist && cp examples/plugins/c-echo/plugin.toml dist/ \ && cp examples/plugins/c-echo/libloadr_plugin_cecho.so dist/ loadr plugin install dist loadr run examples/34-c-echo.yaml Or point the plan's `plugins:` entry at the built artifact directly: plugins: - { name: cecho, path: examples/plugins/c-echo/libloadr_plugin_cecho.so }
A protocol plugin written in Go, loaded over loadr's frozen C ABI. Like the c-echo example, go-echo is NOT a Rust plugin: it's a Go shared library (`go build -buildmode=c-shared`) exporting the four `extern "C"` symbols of loadr's C ABI — proving loadr plugins can be authored in Go. The host auto-detects the C ABI at load time. It serves the `goecho://` scheme and echoes each request body back, status 200. Build + install the plugin, then run: make -C examples/plugins/go-echo mkdir -p dist && cp examples/plugins/go-echo/plugin.toml dist/ \ && cp examples/plugins/go-echo/libloadr_plugin_goecho.so dist/ loadr plugin install dist loadr run examples/35-go-echo.yaml Or point the plan's `plugins:` entry at the built artifact directly: plugins: - { name: goecho, path: examples/plugins/go-echo/libloadr_plugin_goecho.so }
Per-VU auth tokens with automatic refresh — Artillery's `generating-vu-tokens` + `refresh-auth-token` in one place. Each VU mints its own token the first time it needs one (scripts/auth.js, beforeRequest hook), attaches it to every request, and silently re-mints it just before it expires. A custom `token_refreshes` counter tracks how often that happens. Swap the synthetic mint() for a real call to your auth server. loadr run examples/36-auth-tokens.yaml
Multipart file uploads — Artillery's `http-file-uploads`. A POST with a multipart/form-data body that mixes literal fields with a file part read from disk. `file:` streams the file contents; `filename` and `content_type` set the part headers. Values support `${...}` interpolation. loadr run examples/37-file-uploads.yaml
Cookie-based sessions — Artillery's `using-cookies`. loadr keeps a per-VU cookie jar (on by default). `Set-Cookie` from the login response is stored and replayed automatically on every later request in that VU's flow, so a server-side session "just works". You can also read/write/clear cookies from JS (session.cookieGet / cookieSet / cookiesClear). loadr run examples/38-cookies-session.yaml
User-defined metrics — Artillery's `track-custom-metrics`. Declare your own metrics under `metrics:` and emit to them from JS: counter -> session.counterAdd(name, n) (monotonic totals, e.g. £ booked) rate -> session.rateAdd(name, bool) (proportion true, e.g. cache hits) trend -> session.trendAdd(name, value) (distribution, e.g. a custom latency) gauge -> session.gaugeSet(name, value) (last value, e.g. queue depth) Tag requests by endpoint so the built-in HTTP metrics break down per route too. loadr run examples/39-custom-metrics.yaml
Weighted traffic mix — Artillery's `scenario-weights` (Locust @task weights / Gatling randomSwitch). A single scenario whose flow picks one weighted branch per iteration: 70% just browse, 20% search, 10% complete a checkout. Each branch is tagged (`branch`) so per-path metrics fall out automatically. Tune the weights to model real traffic shape without spinning up separate scenarios. loadr run examples/40-scenario-weights.yaml
Functional / smoke testing — Artillery's `functional-testing-with-expect` + `table-driven-functional-tests`. loadr doubles as a functional tester: make one pass over a table of cases (data feeder with `on_eof: stop`), assert each response, and let the run exit non-zero if a single check fails (`checks: rate>=1.0`). Drop it straight into CI as a contract/smoke gate — no separate test framework needed. loadr run examples/41-functional-test.yaml # exit code 99 on any failed check
SOAP / XML web services — Artillery's `soap-with-custom-function`, natively. loadr sends the raw SOAP envelope as a string body and pulls values back out of the XML response with XPath extractors and checks (no custom function needed). `local-name()` keeps the XPath namespace-agnostic. loadr run examples/42-soap.yaml
Twirp RPC — Artillery's `rpc-twirp-with-custom-function`, natively. Twirp speaks plain HTTP: POST to /twirp/<pkg.Service>/<Method> with a JSON (or protobuf) body. loadr's JSON body sets `Content-Type: application/json`, which is exactly Twirp's JSON mode — so a Twirp call is just an HTTP request. (For protobuf mode, send `body: { file: req.bin }` with a protobuf Content-Type.) loadr run examples/43-twirp.yaml
Regression gating — `loadr compare` diffs two summary exports and fails CI when the current run is worse than the baseline. The workflow: run the SAME plan twice (baseline vs candidate), export each summary, then diff: loadr run examples/44-compare-and-gate.yaml --summary-export base.json # ...deploy the candidate build... loadr run examples/44-compare-and-gate.yaml --summary-export current.json loadr compare base.json current.json p95, p99, error_rate and the checks pass_rate gate at a 5% default tolerance; every other field is informational. Tune gates per field or per metric, and make regressions fail the pipeline (exit 99) with --assert: loadr compare base.json current.json --assert \ --max-regression p95=10% \ --max-regression http_req_duration.p99=25 \ --max-regression error_rate=0.5 \ --max-regression rps=5% Percent limits are relative to the baseline; bare numbers are absolute in the field's display unit (milliseconds for latency, percentage points for rates). PR-comment markdown and machine-readable JSON come for free: loadr compare base.json current.json --markdown compare.md --output compare.json
Capacity sweep — `loadr sweep` runs ONE plan across a parameter matrix and tabulates p50/p95/p99, error rate and RPS side by side, one row per combo: loadr sweep examples/45-sweep.yaml --var vus=10,25,50 --duration 30s Repeated --var axes multiply into a cartesian matrix. Every value is exported to the plan's environment as LOADR_SWEEP_<NAME> (and `vus` / `duration` additionally map onto the run overrides), so custom knobs are plain ${env.*} / __ENV reads: loadr sweep examples/45-sweep.yaml \ --var vus=10,50 --var page_size=25,100 \ --out-dir sweep-results --markdown sweep.md Per-combo summary exports land in --out-dir (default: loadr-sweep/), so any two cells of the matrix can be diffed later with `loadr compare`.
SLO-style thresholds: `slo(N%) < bound` reads as "N% of requests finish within the bound" — the way SLOs are written in the reliability doc, with no percentile translation in your head. slo(99%) < 300ms == p(99) < 300. Supported objectives match the fixed points the histogram summary carries: 50, 90, 95, 99, 99.9 (the % sign is optional). Bounds accept duration units (300ms, 1.5s) or plain milliseconds, and the classic `p(95)<500` syntax mixes in freely. loadr run examples/46-slo-thresholds.yaml
Was it the target — or your load generator? An `observe` collector samples the local host's CPU / memory / disk / network from /proc every second during the run (Linux; other platforms log a warning and skip) and overlays the series on the run timeline — in the HTML report, the web UI and the summary export. Observed series are ordinary metrics: system_cpu, system_memory, system_disk_io, system_network (rename the family with `as_prefix`). CPU and memory are 0..1 ratios; disk I/O and network are bytes per second. They are valid threshold targets, so a saturated generator FAILS the run instead of silently skewing the latency numbers. loadr run examples/47-system-observe.yaml
Chaos drill — what does the client-side picture look like when the backend misbehaves? Steady traffic runs its normal flow while a second scenario keeps forcing server errors and multi-second stalls (go-httpbin's /status and /delay endpoints stand in for a flaky dependency). Scenario-scoped thresholds judge the blast radius on the steady traffic only, and the abort threshold is the circuit breaker: if errors explode, the run stops instead of burning the target. docker compose -f examples/harness/docker-compose.yml up -d --build loadr run examples/48-chaos.yaml Coordinated fault injection (network partitions, node kill, added latency, clock skew) is the `loadr chaos` RFC — see docs/design/concurrency-consistency-testing.md. The planned schedule shape, for when the nemesis lands: nemesis: seed: 1234 # reproducible fault schedule faults: - { at: 20s, do: partition, groups: [[node-1], [node-2, node-3]], for: 25s } - { at: 60s, do: kill, node: primary, for: 15s } - { every: 30s, do: latency, node: any, ms: 200, jitter: 50, for: 10s } heal_at_end: true
Algorithmic-complexity (DoS) probe. The request body is a generated adversarial payload whose depth is the swept `depth` axis (exported to the run as LOADR_SWEEP_DEPTH). Run it under `loadr sweep` with `--complexity depth` to fit response-time vs depth and `--max-exponent 1.2` to fail CI when a parser scales worse than quasilinear: loadr sweep examples/49-payload-complexity.yaml \ --var depth=4000,8000,16000,32000,64000 \ --complexity depth --max-exponent 1.2 See `loadr payload --list` for every payload kind.