In k6, what is the difference between the built-in vus and vus_max metrics?
answer
- two levels, not two totals
- active versus initialised populations
- the only two built-in Gauges
- scheduler ticks once per second
- vus never exceeds vus_max
basics
~20 sk6 registers vus and vus_max as built-in Gauges, emitted once per second by its scheduler. vus reports the virtual users currently active; vus_max reports the virtual users k6 has initialised, so vus never exceeds vus_max.
solid answer
~30 s`vus` and `vus_max` are the only two Gauges among k6's built-in metrics, and they count different populations: `vus` is how many virtual users are active at that moment, `vus_max` is how many VU instances k6 has initialised. k6's scheduler emits a sample of each on a one-second ticker, so both exist in every run — including one that never makes an HTTP request. Being Gauges, neither accumulates: each reports a level, not a total. `vus` therefore never exceeds `vus_max`, and neither is a count of in-flight requests — that tally is `http_reqs`.
go deeper
Remember that both are Gauges and that one counts active virtual users while the other counts initialised ones. Knowing vus never exceeds vus_max is enough to answer most screens.
Explain that k6's scheduler emits both on a one-second ticker regardless of protocol, and that a Gauge reports a level so neither metric accumulates across the run.
Point out the one-second sampling resolution when someone reads a VU curve too finely, and separate vus from request-level tallies such as http_reqs when diagnosing a run.
Know that vus and vus_max are the only Gauges k6 registers, so any convention phrased as 'the VU number' has to say which of the two built-in names it means.
## Two Gauges that count different populations `vus` and `vus_max` are both built-in **Gauge** metrics that k6 registers itself, and the whole difference between them is which population of virtual users each one counts: - **`vus`** — the number of virtual users **currently active**, that is, executing right now. - **`vus_max`** — the number of virtual users k6 has **initialised**, that is, VU instances that exist and are ready to be used. An initialised VU is not necessarily a running one. `vus_max` is therefore an upper bound that `vus` moves within, and `vus <= vus_max` holds for the whole run. ## How and when k6 emits them Both metrics are produced by k6's scheduler rather than by anything in your script: 1. The scheduler starts a **one-second ticker** when the run begins. 2. On every tick it builds two samples — one for `vus` from the currently-active count, one for `vus_max` from the initialised count. 3. Both samples carry the run's tags and the same timestamp. 4. The ticker stops when the run's execution context ends. Three consequences follow: - **They are protocol-independent.** A run that makes no HTTP request at all still produces `vus` and `vus_max` samples, unlike `http_reqs` or `http_req_duration`. - **Their resolution is one second.** k6 samples the two levels on the tick, not on every VU state change, so a change shorter than the tick interval may never appear as a distinct value. - **They are emitted throughout**, so a Gauge's latest value tracks the run rather than summarising it. ## The two side by side | | `vus` | `vus_max` | |---|---|---| | Type | Gauge | Gauge | | Counts | Virtual users currently active | Virtual users k6 has initialised | | Source | Currently-active VU count | Initialised VU count | | Emitted | Once per second by the scheduler | Once per second by the scheduler | | Relationship | Never exceeds `vus_max` | The ceiling `vus` moves within | | Depends on protocol | No | No | ## Reading them without misreading them Because both are Gauges, neither accumulates. k6's Gauge type keeps the smallest, the largest and the latest value it saw — so a `vus` figure is a level at a moment, never a total of users seen across the run. The common errors are all versions of forgetting that: - **`vus` is not a running total of VUs the test used.** Ten users active at one instant reports ten, regardless of how many iterations they have collectively run. - **`vus_max` is not a configured option echoed back.** It is the count of VU instances k6 has actually initialised at the moment of the sample. - **`vus` is not the count of in-flight requests.** An active VU may be sleeping or between requests; the request tally is `http_reqs` and the completed-iteration tally is `iterations`. - **Neither is `vus_active`.** That name does not exist in k6; the active count is `vus`. ## The other execution built-ins around them `vus` and `vus_max` belong to the group of built-ins k6 emits regardless of protocol. The others in that group answer different questions and are worth keeping distinct: - **`iterations`** — a **Counter** of completed iterations of the default function. - **`iteration_duration`** — a **Trend** timing one complete iteration. - **`dropped_iterations`** — a **Counter** of iterations that were never started. - **`data_sent`** and **`data_received`** — **Counters** of bytes written and read. Only the first two Gauges report a *level*; each of the rest either accumulates or distributes. That is the quickest way to tell at a glance what kind of statement a k6 built-in is making: a Gauge answers "how many right now", a Counter answers "how many so far", a Rate answers "what fraction", and a Trend answers "how were the values spread". ## Where they sit among the built-ins `vus` and `vus_max` are the only two Gauges among k6's built-in metrics. Everything else built in is a Counter (`iterations`, `http_reqs`, `dropped_iterations`, `data_sent`, `data_received`), a Rate (`checks`, `http_req_failed`), or a Trend (`iteration_duration`, `group_duration`, all the `http_req_*` timings, `grpc_req_duration` and the WebSocket timings). If a k6 built-in reports a *level* rather than a tally, a proportion or a distribution, it is one of these two.
- Does a k6 run that makes no HTTP requests still emit vus and vus_max?Yes. Both are emitted by k6's scheduler on a one-second ticker, independently of any protocol module. `http_reqs` and the `http_req_*` timings only appear when HTTP requests happen, but `vus` and `vus_max` are present in every run.
- How often does k6 sample vus, and what does that mean for short-lived changes?Once per second, on the scheduler's ticker. k6 reads the current levels at each tick rather than reacting to every VU state change, so a change that begins and ends inside one tick interval may never appear as its own sampled value.
- Is vus a count of requests currently in flight?No. `vus` counts virtual users that are active, and an active VU may be between requests or sleeping. The request tally is the `http_reqs` Counter, and completed iterations are counted by `iterations`.
saying these in an interview costs you the question
- Describing vus as a running total of virtual users across the run
- Treating vus_max as an option value echoed back rather than a measured count
- Assuming vus counts in-flight requests instead of active virtual users
- Expecting vus and vus_max only in runs that make HTTP requests
- Using the name vus_active, which does not exist in k6