Why does k6 report http_req_connecting and http_req_tls_handshaking as 0 for most requests in a run?
answer
- same metric, two very different requests
- pooled connections change what is timed
- nothing to dial, nothing to handshake
- zero is a recorded sample, not missing
- first request per VU pays the setup
basics
~10 shttp_req_connecting and http_req_tls_handshaking are built-in Trends timing the TCP connect and the TLS handshake. A request served by an already-open pooled connection has neither to time, so k6 records 0 on both.
solid answer
~40 s`http_req_connecting` times the TCP connect and `http_req_tls_handshaking` times the TLS handshake, and k6 emits a sample on each Trend for every request. If the request is served by a connection already in k6's pool, no dial and no handshake happen: k6's tracer treats the reused connection as having start and end instants at the same moment, so both metrics record `0`. In practice only the first request a VU makes to a host pays setup, and every later request on that connection reads zero — hence near-zero aggregates. Plain HTTP never yields a non-zero `http_req_tls_handshaking` at all. Neither metric is part of `http_req_duration`.
code
javascript · 8 linesimport http from 'k6/http';
export default function () {
const first = http.get('https://quickpizza.grafana.com/');
const second = http.get('https://quickpizza.grafana.com/');
console.log(`first connecting=${first.timings.connecting} tls=${first.timings.tls_handshaking}`);
console.log(`second connecting=${second.timings.connecting} tls=${second.timings.tls_handshaking}`);
}go deeper
Know that k6 emits http_req_connecting and http_req_tls_handshaking on every HTTP request, and that a reused connection makes both read zero. A zero is a real recorded value.
Explain the mechanism: k6's HTTP transport pools connections, and when one is reused the tracer has no dial or handshake window to measure, so it records zero on both Trends.
Show you can pick one call apart with res.timings when aggregates mislead, and that you know which of k6's timing metrics move on a first request and which stay flat regardless.
Be explicit about which k6 built-ins a shared convention names, since http_req_duration and the three setup Trends are separate metrics and quoting one of them is not quoting the others.
## What the two setup metrics actually record `http_req_connecting` and `http_req_tls_handshaking` are built-in **Trend** metrics that k6 emits for every HTTP request, alongside `http_req_duration`. In k6's own words: - **`http_req_connecting`** — time spent establishing the TCP connection to the remote host. - **`http_req_tls_handshaking`** — time spent performing the TLS handshake with that host. Neither is part of `http_req_duration`, which k6 computes as `http_req_sending + http_req_waiting + http_req_receiving`. They are recorded separately precisely because a request does not always pay them. ## The connection-reuse rule k6 issues requests over a pooled HTTP transport. When a request is handed a connection that is already open, there is no dial to time and no handshake to time, so k6 records **0** on both Trends for that request. The zero is deliberate, not an artefact: when k6's HTTP tracer is told the connection was reused, it stamps the connect-start and connect-done instants to the same moment, and does the same for the TLS handshake instants when the connection is a TLS one. The subtraction that follows therefore yields exactly zero. The consequence for a run of any length: - The **first** request a virtual user makes to a host pays the setup and produces non-zero `http_req_connecting`, and non-zero `http_req_tls_handshaking` if the scheme is HTTPS. - **Every subsequent** request that lands on the same live connection reports `0` on both. - Over thousands of iterations the overwhelming majority of samples on both Trends are `0`, which is why their aggregates look near-zero even in a run that opened many connections. - A plain `http://` target never produces a non-zero `http_req_tls_handshaking` at all, because no handshake takes place. ## One call versus the next A sample of k6 metric output in k6's own documentation, covering two successive requests to the same host, shows the pattern directly — the second request keeps a comparable `http_req_duration` while its setup metrics collapse: | Metric | First request | Second request | |---|---|---| | `http_req_blocked` | 558.983 | 0.015 | | `http_req_connecting` | 54.135 | 0 | | `http_req_tls_handshaking` | 477.198 | 0 | | `http_req_duration` | 117.003 | 119.122 | The duration barely moves. All of the extra cost on the first call sat in metrics that `http_req_duration` does not contain. ## `http_req_blocked`, the third setup metric The same family holds `http_req_blocked` — a Trend covering the time k6 waited to acquire a connection before it could initiate the request. It behaves like its two siblings in the sense that it is also excluded from `http_req_duration`, and it also collapses to a tiny value once a pooled connection is immediately available. It is the metric to look at when a request was held up before any of the connect or handshake work began. ## Reading a per-call breakdown Each of these names has a field of the same shape on the response object `k6/http` returns, so you can watch one call rather than an aggregate: 1. Capture the response: `const res = http.get(url)`. 2. Print `res.timings.connecting` and `res.timings.tls_handshaking` — the per-call values that k6 fed into `http_req_connecting` and `http_req_tls_handshaking`. 3. Repeat the request in the same iteration and print them again; the second pair reads `0`. 4. Print `res.timings.blocked` too, to see the pre-connection wait separately. ## Every request emits every one of them It is worth being precise about what "0" means here, because the alternative reading — that k6 skipped the measurement — is the misconception this question exists to catch. For **each** HTTP request, k6 emits exactly one sample on each of the seven timing Trends (`http_req_blocked`, `http_req_connecting`, `http_req_tls_handshaking`, `http_req_sending`, `http_req_waiting`, `http_req_receiving`, `http_req_duration`), plus one on the `http_reqs` Counter and one on the `http_req_failed` Rate. That is true whether the request opened a connection or inherited one. All of those samples share a single timestamp, taken at the end of the request. So a run of ten thousand requests over a hundred connections produces ten thousand samples on `http_req_connecting` — of which roughly nine thousand nine hundred are zeros. The metric is fully populated; it is the values that are zero. ## What this does not mean Two misreadings are common and both are wrong on k6's own terms: - **A zero does not mean k6 failed to measure the phase.** k6 emitted a sample with the value `0`, and that sample is in the Trend like any other. - **A zero does not mean no connection existed.** It means this particular request did not open one; an earlier request in the run did. In k6 v2 all three setup metrics — `http_req_blocked`, `http_req_connecting` and `http_req_tls_handshaking` — are registered by k6 itself, are Trends, and are emitted once per request whether their value is zero or not.
- If http_req_connecting is 0 for a request, does that mean k6 emitted no sample for it?No. k6 emits a sample on `http_req_connecting` for every HTTP request; a reused connection simply gives that sample the value `0`. The metric is populated, the observation is just zero, and it counts towards the Trend like any other value.
- Which built-in k6 metric covers the wait before the connect attempt even begins?`http_req_blocked`, a built-in Trend for the time k6 spent blocked waiting to acquire a connection before it could initiate the request. Like `http_req_connecting` and `http_req_tls_handshaking`, it is excluded from `http_req_duration`.
- Would a run against an http:// target ever record a non-zero http_req_tls_handshaking?No. The metric times a TLS handshake, and a plain HTTP request performs none, so k6 records `0` on every sample. The metric still exists and is still emitted — it is a built-in, not something switched on by the scheme.
saying these in an interview costs you the question
- Reading a zero as k6 failing to instrument the connection phase
- Assuming every request opens its own TCP connection and pays setup
- Expecting non-zero http_req_tls_handshaking on a plain http:// target
- Thinking connection setup inflates http_req_duration on the first call
- Treating http_req_blocked and http_req_connecting as the same measurement