In k6, `rate` is legal on both a Counter and a Rate metric, so what does each one compute?
answer
- same token, two different units
- one is per second, one a share
- elapsed run time is the divisor
- both survive configuration validation
- only the evaluated number gives it away
basics
~20 sOn a k6 Counter, rate is the running total divided by the elapsed run time, so it is events per second and unbounded. On a Rate metric, rate is non-zero samples over total samples, a proportion between 0 and 1. Same token, different units.
solid answer
~40 s`rate` is the one aggregation token k6 shares between two metric types, and it means something different in each. On a **Counter** such as `http_reqs`, k6 divides the accumulated total by the elapsed run duration at the moment of evaluation, giving **events per second** with no upper bound — `http_reqs: ['rate>100']` asserts more than a hundred requests a second. On a **Rate** such as `checks`, k6 divides non-zero samples by total samples, giving a **proportion in `[0, 1]`** — `checks: ['rate>0.99']` asserts that over 99% of checks passed. Because both pairings are type-legal, swapping them survives k6's pre-run validation entirely: `checks: ['rate>100']` starts a full run and can never pass, and `http_reqs: ['rate>0.99']` starts a full run and passes on essentially any traffic. Only reading the evaluated number reveals the mistake.
code
javascript · 14 linesexport const options = {
duration: '1m',
thresholds: {
// Counter: `rate` is requests per second of elapsed run time.
http_reqs: ['rate>100'],
// Rate: `rate` is passed / total, so it never exceeds 1.0.
checks: ['rate>0.99'],
// Both of these are type-legal and both are wrong:
// checks: ['rate>100'], // can never pass
// http_reqs: ['rate>0.99'], // passes above 1 req/s
},
};go deeper
Note that k6 lets the word rate appear on two different metric types, and that on a Counter it counts events per second while on a Rate it reports a share of samples.
Be able to state both formulas: total divided by elapsed seconds for a Counter, non-zero over total for a Rate, and give the range of each.
Demonstrate that you know the swap passes validation, so a rule like http_reqs rate>0.99 is permanently green and hides real failures; describe how you would catch that in review.
Worth raising: a shared token across two types is a naming decision that saved grammar at the cost of a whole defect class, and a team can only recover safety through convention or a lint step of its own.
## One token, two computations k6 derives a threshold's legal aggregations from the metric's type. Two of the four types happen to claim the same token: | Metric type | Expression | k6 computes | Range | |---|---|---|---| | Counter | `rate` | accumulated total ÷ elapsed run seconds | `0` to unbounded | | Rate | `rate` | non-zero samples ÷ total samples | `0.0` to `1.0` | Nothing in the expression distinguishes them. `rate>100` is a throughput floor on `http_reqs` and an impossibility on `checks`, and the only thing that decides which is the type of the metric named on the left of the colon. ## The Counter form: a per-second throughput A Counter sink stores a single running sum. When k6 evaluates a Counter threshold it publishes two numbers: `count`, the sum itself, and `rate`, that sum divided by the elapsed run duration expressed in seconds. Three properties follow: - **The divisor moves.** k6 re-evaluates thresholds on a ticker roughly every two seconds during the run, and each evaluation uses the run duration *so far*. Early in a run the divisor is small, so the computed rate is noisy; by the final evaluation it is the whole elapsed run. - **It is not the configured duration.** A run that aborts early divides by the time actually spent, not by the `duration` you asked for. - **A zero divisor is guarded.** If no time has elapsed, k6 publishes no `rate` value at all rather than producing infinity, and a threshold with no value to compare against is treated as passing. Every built-in Counter shares this behaviour: `http_reqs`, `iterations`, `data_sent` and `data_received`. ## The Rate form: a proportion A Rate sink stores two integers, a total and a count of non-zero samples, and `rate` is their quotient. It is dimensionless and can never exceed `1.0`. `checks` and `http_req_failed` are the built-ins that use it. If no samples arrived at all, the quotient is undefined, so k6 publishes nothing and the rule passes by default — an important asymmetry with the Counter case, where an unsampled counter still resolves `count` to zero and can genuinely breach. ## Why the swap is invisible until it runs k6's pre-run validation asks exactly one question about the left-hand side: does this metric's type support this aggregation token? Both `checks` and `http_reqs` answer yes for `rate`. Nothing examines the right-hand side, so: 1. `checks: ['rate>100']` is accepted, runs the full test, and is breached at the end because the left-hand side never exceeds `1.0`. 2. `http_reqs: ['rate>0.99']` is accepted, runs the full test, and passes on any workload above one request per second — a rule that looks like a 99% success target and is really a near-zero throughput floor. The second is the more dangerous of the two: it is green, so nobody investigates it, and it will stay green through a total collapse of correctness. ## Reading the two apart in the summary The end-of-test summary keeps the distinction visible if you know to look for it. A metric whose type is `rate` is humanised by multiplying its value by 100 and appending a percent sign, so `checks` appears as something like `95.00%`. A Counter is printed as a raw total with its per-second figure alongside, so `http_reqs` appears as a count and a rate in events per second. The threshold lines above the metric block echo the expression exactly as you wrote it, which means a rule reading `rate>100` sitting above a metric rendered as a percentage is a mismatch you can spot in one glance. That reading also exposes the reverse mistake. A `rate>0.99` rule above a Counter rendered in the hundreds or thousands per second is technically satisfied, permanently, and will never draw attention on its own. The summary marks it with a tick like any other passing rule. ## How to keep them apart - Read the right-hand side as a unit test of your intent: a value above `1` on a `rate` expression only makes sense for a Counter; a value below `1` only makes sense for a Rate — or for a very slow Counter, which is worth a comment. - Prefer `count` over `rate` on a Counter when you actually mean a total, so `rate` in a script is a strong hint that a Rate metric is involved. - Check the end-of-test summary: k6 renders a Rate-typed metric as a percentage and a Counter as a raw number with a per-second figure beside it, so the two are easy to tell apart once the run has finished. - When you introduce a custom metric, pick the type from the question you will ask of it. `new Rate('login_ok')` for a share, `new Counter('logins')` for a volume; converting later is not possible, because re-registering a name with a different type is an error.
- Which duration does k6 divide by when evaluating `rate` on a Counter?The elapsed test run duration at the moment of evaluation, not the configured `duration` option. Because k6 re-runs thresholds on a roughly two-second ticker, an early evaluation divides by only a few seconds and can swing wildly; the figure that matters is the final one, computed over the whole run as it actually happened.
- Can a Counter's `rate` threshold silently pass on a run that produced nothing?Yes, in two ways. If no time has elapsed k6 publishes no `rate` value and the rule passes for want of anything to compare. Separately, k6 skips metrics with empty sinks during the in-run evaluations, so a counter that never receives a sample is invisible until the final pass — where `rate` resolves to zero and a floor such as `rate>100` does then breach.
saying these in an interview costs you the question
- Assuming rate always means a success proportion
- Writing rate>99 on a Rate metric and expecting 99 percent
- Thinking a Counter's rate is bounded by 1.0
- Believing k6 divides a Counter by the configured duration
- Expecting validation to catch a rate rule with an implausible value