The problem
One k6 process runs on one machine. When a test needs more load than that machine can produce, k6 offers execution segments, which split a test exactly across instances. What it does not offer is anything that treats those instances as one test. Issue #140, "Clustering and distributed execution", has been open since March 2017 and has 103 thumbs up. A maintainer wrote there in 2021 that distributed tests work with segments, "unfortunately, without thresholds and a centralized summary". Two proofs of concept for native support were closed unmerged, the second in May 2026.
The existing routes are the Kubernetes operator, the hosted Grafana Cloud service, or own scripts. A code search finds threshold handling in the operator only in its Grafana Cloud path.
Missing merged data changes results. If one of three generators gets the slow requests, the other two pass p(95)<300 on their own samples while the merged p(95) fails.
How it works
k6-fleet is a Go binary without dependencies outside the standard library. The controller runs k6 archive, so script, imports and open() files travel as one file, and reads the thresholds with k6 inspect. It starts itself as an agent on each machine over ssh or Docker. All communication runs over the agent's stdin and stdout, so no port is opened on a generator.
Each agent starts k6 paused with its execution segment and --no-thresholds. setup() runs once through k6's REST API, and its result goes to the other instances. The controller measures clock offsets and gives all agents a common start time. Agents read k6's sample stream from a FIFO and send one frame per second with counters, rates and a mergeable quantile sketch per Trend. The controller merges these, applies abortOnFail, runs teardown() once and exits with k6's exit codes.
Output from CI with real k6 v2.3.0 and three agents. The script adds each iteration number from 0 to 999 to a Trend, so the exact values are known (average 499.5, p(95) 949.05):
THRESHOLDS (evaluated on merged data)
ok deterministic "avg==499.5" actual=499.5
ok deterministic "max==999" actual=999
ok deterministic "p(95)<960" actual=948.7
AGENTS
name segment iterations requests
local-0 0:1/3 334 336
local-1 1/3:2/3 333 333
local-2 2/3:1 333 333
How it is tested
The orchestration scenarios run against a stub of the k6 CLI, REST API and JSON output. The stub could not show what real k6 does, so CI runs the same flows against k6 v2.3.0 and v1.8.2 with a target that records what arrived. It checks that 1000 shared iterations arrive exactly once, that counts match a single k6 run and percentiles stay within 0.5 percent of it, and that kill -9 on an agent ends the run with exit 3 and removes its k6. The ssh and Docker transports run in the same jobs.
The first run against real k6 found two things the stub had hidden. k6 2.x removed --no-summary. And k6 1.x does not report an exit code through its API, so exec.test.abort() on one instance looked like a clean end and the other agents kept running.
Limits
All tests ran on one machine per CI job, so a fleet of separate hosts over a real network is untested. The JSON stream costs throughput. With k6, target and agent on a shared 4-vCPU runner, one agent reached 48 percent of plain k6's requests per second on v2.3.0 and 87 percent on v1.8.2, and most of that loss came from --out json alone. Runner speed varied by more than a factor of two, so only these ratios within a job mean anything. Percentiles have up to 0.5 percent relative error, and handleSummary() is not called.
The code is at github.com/Jwrede/k6-fleet.