Before running
Run from a remote machine, not your laptop. Client CPU contention inflates http_req_tls_handshaking — can show 3s p99 when actual TLS cost is 58ms.
Stabilise the environment first. Check k9s for pods with high restart counts before starting. A pod restarting mid-test makes results uninterpretable.
Connection reuse
k6 has two separate options controlling connection reuse:
| Option | Scope | Effect when true |
|---|---|---|
noConnectionReuse | Within an iteration | A fresh TCP connection is opened for every HTTP request, even within the same iteration |
noVUConnectionReuse | Between iterations | Connections are cleaned up after each iteration; within an iteration, keep-alive still applies |
By default both are false: k6 reuses connections within an iteration and carries them over to the next iteration of the same VU.
The between-iteration race condition occurs when a server-side idle timeout fires on a kept-alive connection while the VU is between iterations:
- The server decides an idle connection has timed out and sends a TCP FIN to close it.
- While that FIN is still in-flight (or sitting unprocessed in the OS receive buffer), the VU sends a new request on the same socket.
- The server receives the request on an already-closed socket and responds with TCP RST.
- k6 sees a “connection reset by peer” or EOF — which looks like a server error but is just a connection lifecycle mismatch.
The root cause is a timeout mismatch: as long as the client timeout ≥ server timeout, the server can close first and trigger this race. Use noVUConnectionReuse: true to fix it — connections are reset after each iteration but reused within it, so the overhead is lower than noConnectionReuse.
TLS in UAT
export const options = {
insecureSkipTLSVerify: true,
};UAT environments might have inconsistent certs across pods. Without this, you get spurious TLS errors that look like server failures.
Tagging for per-endpoint metrics
k6 automatically applies a scenario system tag to every metric, but scenario tags don’t create per-endpoint sub-metric breakdowns — they just let you filter by scenario after the fact. To get http_req_waiting{test:X} scoped to a specific request, you must tag at the request level:
http.get(url, { tags: { test: 'my-endpoint' } });Scenario-level tags (set in options.scenarios[*].tags) propagate to all requests in that scenario but still require a matching threshold to surface as a sub-metric in the summary. Request-level tags are more precise and are the correct unit for per-endpoint breakdown.
Exposing sub-metrics in handleSummary
In the open-source k6 CLI, sub-metrics are only computed and included in the handleSummary data object when a threshold is defined for that tag combination. Without a threshold, k6 doesn’t materialise the sub-metric at all — it won’t appear in the summary even if requests were tagged correctly.
Use a dummy threshold that always passes to force the sub-metric to appear without enforcing any pass/fail condition:
export const options = {
thresholds: {
'http_req_waiting{test:my-endpoint}': ['p(99)>=0'], // always true, never fails
},
};See also
- k6-http-metrics — interpreting and diagnosing HTTP metrics
- k6-large-tests — capacity, OS tuning, script optimisations, distributed execution
- k6-vu-saturation — load generator as the bottleneck