YAML plans and k9.plan
Python builds the plan once, before the run. Templates, extractors and feeders keep data flowing with no Python in the hot path.
Write tests in Python or YAML, with the executors, checks, thresholds and metric names you know from k6. The part that sends requests and measures time is Rust, so Python only describes the work.
★ k6's requests/s
Locust's single process
Arrival rates included
Dual-licensed, your pick
Measured on one machine (i9-13900K, Linux under WSL2). How we measured →
import k9
options = {
"vus": 50,
"duration": "1m",
"thresholds": {"http_req_duration": ["p(95)<300"]},
}
async def default():
res = await k9.http.get("https://test.k6.io/")
k9.check(res, {"status is 200": lambda r: r.status == 200})
options:
vus: 10
duration: 30s
thresholds: { http_req_failed: ["rate<0.01"] }
flows:
default:
- request:
method: POST
url: https://example.com/login
body: '{"user": "{{ users.name }}"}'
extract: { token: { json: $.token } }
- request:
url: https://example.com/me
headers: { Authorization: 'Bearer {{ token }}' }
checks: { ok: { status: 200 } }
import k9.plan as p
token = p.var("token")
# Python builds the plan once; the run has no Python in it.
plan = p.Plan(
[
p.post("https://example.com/login", json={"user": "bob"},
extract={"token": p.from_json("$.token")}),
p.get("https://example.com/me",
headers={"Authorization": f"Bearer {token}"},
checks={"ok": p.status(200)}),
],
options={"vus": 10, "duration": "30s"},
)
import pytest
# Crossed thresholds fail the test.
@pytest.mark.k9("load/smoke.py", vus=10, duration="30s")
def test_checkout_holds_up(k9_result):
assert k9_result.metric("http_req_duration")["p(95)"] < 300
def test_two_runs(k9_run):
assert k9_run("load/a.yaml").ok and k9_run("load/b.yaml").ok
running (00m10.1s), 00/10 VUs, 990 complete and 0 interrupted iterations
data_received......: 129 kB 13 kB/s
data_sent..........: 70 kB 7.0 kB/s
http_req_duration..: avg=101.88ms med=101.79ms p(95)=102.62ms
http_req_failed....: 0.00% ✓ 0 ✗ 990
http_reqs..........: 990 98.100557/s
iteration_duration.: avg=101.9ms p(95)=102.62ms
iterations.........: 990 98.100557/s
vus................: 10 min=10 max=10
# add thresholds: crossing one exits with code 99, as in k6
k6's script shape and metrics, Goose's engine ideas, and Python for the tests. If Locust serves you well, keep using it: K9 is for when the load generator's own overhead or accuracy matters.
| k6 | Locust | Goose | ⚡ K9 | |
|---|---|---|---|---|
| Test language | JavaScript | Python | Rust (compiled) | Python |
| Engine | Go | Python (gevent) | Rust | Rust |
| Open-model (arrival-rate) executors | ✓ | ✗ closed model | ✗ | ✓ |
| Coordinated omission | Avoided by arrival rates | ✗ | ✓ corrected | ✓ both |
| Load tests inside pytest | ✗ | ✗ | ✗ | ✓ |
| License | AGPL-3.0 | MIT | Apache-2.0 | MIT / Apache-2.0 |
k9 inspect tells you which tier a test runs in, so you know what it costs before you run it.
k9.planPython builds the plan once, before the run. Templates, extractors and feeders keep data flowing with no Python in the hot path.
setup, vu_init, teardownLog in once per VU, prepare data, write your own summary with handle_summary. Each runs once, so its cost doesn't grow with the load.
async def default()Full Python on every iteration: thousands of async VUs on one event loop, or plain def on a thread per VU, across cores with --workers.
Grafana dashboards and CI configs built for k6 carry over: same metric names, same tags, same exit codes.
All seven executors, the threshold syntax with abortOnFail, k6's options, environment variables and exit codes.
A hyper client with k6's per-phase timings, HTTP/2 by ALPN, experimental HTTP/3, WebSockets and gRPC unary calls and streams.
k6's summary, a live terminal dashboard and a self-contained HTML report, and samples streamed to your metrics backend.
--workers runs a test in one process per core; --agents spreads it across machines over gRPC. Totals stay exact.
Arrival-rate executors report iteration_duration_corrected, so a stalled server shows up, and k9 warns when it is itself the bottleneck.
k9 new writes a starter for each tier; k9 convert turns a browser's HAR recording into a plan, tokens and CSRF fields included.
A declarative plan, 100 VUs, a fast local endpoint.
The same load from one Python process, async def VUs.
A Python script on 8 workers at 256 VUs.
1,000 VUs on a 200 ms endpoint, one process, p95 203.6 ms.
Release build on an i9-13900K (24 cores) with Linux under WSL2. Details and the comparison with k6, oha and Locust: benchmarks.
Install the wheel (Linux, macOS, Windows; no Rust needed), let k9 new write the test, and point it at your service.
$ pip install k9-load # or: uv tool install k9-load
$ k9 new --url http://localhost:3000/health # a starter script.py
$ k9 run script.py -u 5 -d 10s