Changelog¶
0.1.0 (2026-10-21)¶
The first release: pytest-native, accurate and fast.
- k6-compatible core. Plans and scripts use k6's option names, all seven
executors, checks, thresholds (exit code 99,
abortOnFail), k6's metric names, its end-of-test summary,--summary-export, JSON Lines output and option overrides (flags >K9_*environment > script). - Accurate HTTP. hyper with a timed connector: k6's blocked, connecting,
TLS, sending, waiting and receiving phases, redirects, cookies and TLS
options. Arrival-rate executors with
dropped_iterations, plusiteration_duration_correctedfor coordinated omission. - Python, in three tiers. Tier 1:
k9.planbuilds declarative flows with templates, extractors, feeders and generators, which run with no Python. Tier 2:setup,teardown,vu_initandhandle_summaryhooks. Tier 3:async def default()on one event loop, or plaindefon a thread per VU, withk9.http,check,group,sleepand custom metrics. - Scale.
--workers Nsplits a test across processes with k6's execution segments. Totals stay exact, and hooks run once. - From Goose. Weighted transactions with
onStart/onStop, with an exact mix per VU. - Library and pytest.
k9.run()returns aRunResult, and raisesThresholdsFailed,ConfigErrororScriptError, allK9Errors. The pytest plugin adds@pytest.mark.k9(...)and thek9_resultandk9_runfixtures. - Requirements. Python 3.10 or newer; wheels for Linux (x86_64, aarch64), macOS (arm64, x86_64) and Windows (x86_64). Building from source needs Rust 1.99 or newer.
Also in this release¶
k9 new [FILE] [--template yaml|plan|async|sync] [--url URL] [--force]: a starter plan or script for each tier, ready to run.k9 convert recording.har [-O plan.yaml]: a HAR recording into a plan, with pages as groups, pauses as sleeps and status checks. Values a response handed out (tokens, ids, CSRF fields) are correlated intojson,headerorregexextractors and{{ variable }}templates.--only/--skipfilter hosts,--no-staticleaves out assets,--no-correlatekeeps recorded values.--out statsd[=ADDR]and--out datadog[=ADDR](DogStatsD tags): samples as StatsD lines over UDP, like k6's outputs, withK6_STATSD_*/K6_DATADOG_*settings.--out prometheus-rw:TREND_AS_NATIVE_HISTOGRAMsends trends as native histograms (k6_<name>_seconds), andSTALE_MARKERSmarks every series stale at the end of the test, as in k6.--out influxdb[=URL](InfluxDB v1, like k6's output) and--out xk6-influxdb[=URL](InfluxDB v2, like k6's extension): every sample as a line-protocol point named like k6's, withtagsAsFields, precision, retention policy, credentials and tokens fromK6_INFLUXDB_*/K9_INFLUXDB_*. The test server receives InfluxDB writes.- gRPC streams:
recv(timeout=)(async and sync) raisesTimeoutErrorwithout losing the message, also whenasyncio.wait_forinterruptsrecv(); agrpc_stream_durationtrend.k9.grpc.Client(metadata=)sends default metadata with every call and stream. - gRPC: gzip message compression (
compression: gzip,Client(compression=); gzipped answers are always read) and k6'smaxSendSize/maxReceiveSize(RESOURCE_EXHAUSTEDpast them), for calls and streams. The test server's Echo service accepts and sends gzip. k9 agent --workers N|autoruns an agent's part of the test in several processes, andk9 agent --weight W|autogives agents unequal shares (e.g. by core count); the manager prints each agent's share.- Agents get a script's local modules (found by reading its imports) and a
plan's
.protofiles (compiled on the manager) and multipart uploads, so they need none of the manager's files. The worker protocol is now version 4. --http-debug[=full](and thehttpDebugoption,K9_HTTP_DEBUG) dumps requests and responses to stderr;-q/--quiet,--no-summaryand--no-color(NO_COLOR).k9.run()results carry every trend stat (count,p(99),p(99.9), …) whatever the summary shows.- k6's run options
minIterationDuration,noCookiesReset,throw(Python requests with no response raiseRequestError),systemTags(drop the engine's tags not listed) andsummaryTimeUnit(s,ms,us), with k6's CLI flags (-wfor--throw) andK9_*variables. - k6's network options:
hosts(name,name:portor*.domainto an IP or IP:port),dns(ttl,select,policy; object or k6's text form),blockHostnames,blacklistIPs(IPs, CIDR blocks,first-lastspans),localIPs(handed out to VUs in turn),noConnectionReuseandnoVUConnectionReuse, with k6's CLI flags andK9_*variables. Blocked requests fail with error codeblocked. The test server hasGET /ip. -
k6's TLS options:
tlsAuth(client certificates per domain, PEM text or files; HTTP, WebSockets and gRPC),tlsVersion(one version or{min, max}; TLS 1.2 and 1.3) andtlsCipherSuites(IANA names). HTTPS samples get k6'stls_versiontag; Python responses havetls_versionandtls_cipher_suite. The test server has a mutual-TLS listener (--identity-dirwrites the client certificate it accepts). -
batchsteps send requests in parallel (k6'shttp.batch), within k6'sbatchandbatchPerHostoptions (--batch,--batch-per-host,K9_BATCH,K9_BATCH_PER_HOST);k9.plan.batch(). -
Compression, as in k6: requests send
Accept-Encoding: gzip, deflateby default and compressed responses (gzip, deflate, br, zstd) are decoded (data_receivedstill counts the wire bytes); a body that doesn't decode fails the request (decompress). Request bodies can be compressed:compression=ink9.http,compression:in plans. -
Multipart uploads:
files=ink9.http/k9.sync.http,multipart:in plans (text fields with templates, files read before the run), andk9.plan.file(). k9-httpcompressionandmultipartmodules; the test server gained/compressed/{encodings}and/multipart. -
gRPC streams (client, server, bidirectional):
client.stream()ink9.grpc(async:send,close_send,recv,async for,cancel) andk9.sync.grpc;messages: [...]on plan steps for streaming methods. Recorded in k6'sgrpc_streams,grpc_streams_msgs_sentandgrpc_streams_msgs_received;rpc_typetags tell the kinds apart. k9-httpClient::grpc_streammultiplexes streams on the shared HTTP/2 connection (a channel-fed request body). -
gRPC steps in declarative plans (
grpc:withtarget,method, templatedmessageandmetadata,status/bodyContains/durationBelowchecks,json/regex/headerextractors), also fromk9.plan.grpc(). Protos come from files (relative to the plan) or the server's reflection service (v1, falling back to v1alpha), loaded once before the run;k9.grpc.reflect()for scripts. The test server serves reflection too. -
Unary gRPC from Python:
k9.grpc.load()compiles.protofiles at run time (protox, noprotoc),k9.grpc.Client(target, protos, plaintext=)andawait client.invoke(method, message, metadata=, tags=, name=, timeout=)with dict messages (protobuf JSON mapping);k9.sync.grpcfor plaindefcode. Recorded as k6'sgrpc_req_duration(tagsmethod,name,url,rpc_type,status). k9-http:grpcmodule (Protos,GrpcRequest,Client::grpc()/grpc_invoke) over its own HTTP/2 (h2c or TLS with ALPN h2), andResponse::trailers. The test server servesk9.test.Echo(tonic). -
k9 run --tui: a live terminal dashboard (ratatui) with stat tiles, charts of requests and response times, checks, thresholds and a log;ppauses,+/-change VUs,qstops.--theme brand|neon, 24-bit colour with 256-colour andNO_COLORfallbacks. The engine publishes aLiveFrameeach second (RunControl::frames()). -
WebSocket steps in declarative plans (
websocket:withsend,receivechecks/extractors,ping,sleep,close; handshake checks), also fromk9.plan.websocket().k9.sync.wsfor plaindefscripts. -
WebSockets from async Python:
k9.ws.connect()(awaitable orasync with),send,recv(timeout=),async for,ping(),close(), recorded in k6'sws_*metrics. k9-http'sClient::websocket(tokio-tungstenite, dialed and timed like requests, HTTP/1.1 ALPN) and k9-core'sScriptContext::websocket. -
HTTP/2: HTTPS negotiates it with ALPN (falling back to HTTP/1.1), and
httpVersion(auto,1.1,2;--http-version,K9_HTTP_VERSION) forces a version, with h2c prior knowledge over plain HTTP. Concurrent requests share one connection as streams with per-stream timings. Theprototag andres.protoreport the version actually used. -
HTTP/3 (experimental):
httpVersion: '3'sends HTTPS requests over QUIC (quinn + h3), one connection per host shared by concurrent streams. The test server answers HTTP/3 on its HTTPS port. -
Distributed runs:
k9 run --agents Nruns the test onk9 agents on other machines. The token is required (--agent-token/K9_AGENT_TOKEN), and TLS is optional (--tls-cert,--tls-key; agents'--tls-ca). Agents receive the script and the feeder rows. - Workers talk to the manager over gRPC (
crates/k9-grpc, a typedworker.proto) instead of postcard frames; protocol version 3. - Python scripts: the asyncio loop now takes commands straight from a queue,
woken through a socket, instead of through a bridge thread. That's 1.6× the
requests/s in one process. Worker processes size their Tokio runtimes to
their share of the CPUs.
benchmarks/compare.py --vus. benchmarks/compare.py: k9 against k6, oha and Locust on one target; results indocs/benchmarks.md.- criterion micro-benchmarks of the hot paths (
cargo bench); results indocs/benchmarks.md. /v1/metrics[/{id}]on the control API, and thek9 status,stats,pause,resume,scaleandstopcommands.- Warnings when the load generator is the bottleneck: open-file limit (raised automatically when possible), ephemeral ports, TIME_WAIT sockets, CPU.
- The
externally-controlledexecutor, and a control API:--addressserves k6'sGET/PATCH /v1/status(pause, resume, scalevus, stop), and--pausedstarts a run paused. --out opentelemetry[=URL](aliasexperimental-opentelemetry): OTLP metrics over HTTP/protobuf, mapped like k6's OpenTelemetry output;K6_OTEL_*/K9_OTEL_*settings.--out prometheus-rw[=URL](aliasexperimental-prometheus-rw): Prometheus remote write with k6's metric names, so k6's Grafana dashboards work;K6_PROMETHEUS_RW_*/K9_PROMETHEUS_RW_*settings.--report PATH(k9.run(report=…)): a self-contained HTML report with the key numbers, thresholds, checks, per-second charts and every metric.--out csv=PATH: every sample as CSV, in k6's columns.--outworks with--workers: workers forward their samples to the manager's outputs (worker protocol v2).summaryTrendStats(plan option,--summary-trend-stats,K9_SUMMARY_TREND_STATS,k9.run(summary_trend_stats=…)) picks the values trends report, such asp(99)orcount.