skip to content

In k6, which k6/metrics types give you p(95) for a checkout step and a pass share for response validation?

level: middleimportance: must knowfreq 70%

answer

  1. one metric type per question asked
  2. only one type keeps every value
  3. false must still reach add()
  4. Trend for duration, Rate for pass share

basics

~20 s

A Trend for the checkout step and a Rate for the validation. Trend is the only type that stores every value, so only it yields p(95); Rate divides non-zero adds by total adds to give the pass share.

solid answer

~40 s

Two custom metrics, one per question. `const checkoutDuration = new Trend('checkout_duration', true)` measures the step: a `Trend` retains every value, so in k6 v2 it is the only type from which `p(95)` — or `avg`, `min`, `max`, `med` — can be computed, and the second argument marks the values as durations so k6 prints them in time units. `const payloadValid = new Rate('payload_valid')` measures the validation: you feed it a boolean every iteration, `add(true)` counts in both the numerator and the denominator while `add(false)` counts only in the denominator, and the reported `rate` is the share, from 0.00 to 1.00. The trap is feeding the validation to a `Counter`: you would get a pass count and a per-second figure, never a share.

code

javascript · 15 lines
javascript
import http from 'k6/http';
import { Trend, Rate } from 'k6/metrics';

const checkoutDuration = new Trend('checkout_duration', true);
const payloadValid = new Rate('payload_valid');

export default function () {
  const started = Date.now();
  http.get('https://quickpizza.grafana.com/');
  const res = http.get('https://quickpizza.grafana.com/api/json');
  checkoutDuration.add(Date.now() - started);

  // every iteration adds one sample, pass or fail
  payloadValid.add(res.status === 200 && res.body.length > 0);
}

go deeper

for a junior

Know the mapping: a duration you want percentiles from goes in a Trend, a pass-or-fail outcome goes in a Rate. Construct both at the top of the script.

for a middle

Explain why - only a Trend retains individual values, and only a Rate keeps a denominator - and show the boolean being added on both the success and failure paths.

for a senior

Point out that the type choice is unrecoverable: once a run has finished, a Counter's sum cannot be turned into a tail figure and a missing false add cannot be reconstructed.

for a principal

Treat the metric set as a design decision made before the run - which k6 types get constructed up front - and weigh a Trend's per-value retention on runs that last hours.

## The scenario A checkout journey is two calls: put an item in the cart, then confirm the order. The report has to answer two different questions about it: 1. **How slow was the checkout step?** — the report has to be able to print `p(95)` for it. 2. **How often did the confirmation payload validate?** — the report has to print a share, not a count of successes. In k6 v2 those are two different metric types, because in `k6/metrics` the type you construct fixes which statistics can ever exist for it. ## A `Trend` for the business step ```javascript const checkoutDuration = new Trend('checkout_duration', true); ``` A `Trend` appends every value it is given to a list. That retention is what makes `avg`, `min`, `max`, `med` and `p(N)` computable at the end of the run — `med` is `p(0.5)`, and a percentile that falls between two stored values is linearly interpolated. No other k6 metric type keeps individual observations, so no other type can produce a percentile at all. Two details matter here: - **Feed it the whole step, not one request.** `checkoutDuration.add(Date.now() - started)` around the pair of calls measures the business step, including the gap between them. That is a different number from any single request's timing, and it is the reason a custom metric exists at all. - **The second constructor argument marks the values as time.** `new Trend('checkout_duration', true)` sets the metric's value type to time, so the summary renders `842` as a millisecond duration rather than a bare float. It changes presentation only — the available statistics are the same either way. ## A `Rate` for the validation ```javascript const payloadValid = new Rate('payload_valid'); ``` A `Rate` holds two integers: how many values were added, and how many of those were non-zero. `add(true)` and `add(1)` bump both; `add(false)` and `add(0)` bump only the total. The reported `rate` is non-zero-adds divided by total adds, so it lives between 0.00 and 1.00 and k6 renders it as a percentage in the end-of-test summary. The single most common bug with a `Rate` follows directly from that arithmetic: **the failing case has to reach `.add()` too.** Writing ```javascript if (res.status === 200) payloadValid.add(true); ``` produces a metric that reads 100% no matter how bad the run was, because failures never enter the denominator. Feed the boolean unconditionally instead: ```javascript payloadValid.add(res.status === 200 && res.body.length > 0); ``` Now every iteration contributes exactly one sample, and the share is real. Three mechanical details are worth holding on to: - **`add(true)` and `add(1)` are the same input**, as are `add(false)` and `add(0)` — k6 converts a boolean to `1` or `0` before the sink sees it, and the sink counts anything non-zero as a success. - **Both `add()` calls take an optional second argument**, an object of tags attached to that single data point rather than to the metric as a whole. - **Neither metric is per VU.** Both are registered once by name for the whole run, so the trend and the rate aggregate every VU's samples together. ## The pairings that do not work | Choice | What you would get | What it costs | |---|---|---| | `Counter` for the checkout step | `count` — the sum of every duration — and a per-second `rate` | the sum of milliseconds is meaningless, and there is no typical or tail value left to read | | `Gauge` for the checkout step | `value` — whatever the last add happened to be | every earlier measurement, discarded on the next add | | `Counter` for the validation | `count` of passes, and passes per second | the denominator: 90 passes cannot be told apart from 90 out of 100 or 90 out of 1000 | | `Trend` for the validation | `avg` of ones and zeros, numerically the same share | a percentage rendering, and the cheapness of two integers instead of one float per iteration | The last row is the interesting one. A `Trend` of booleans is not *wrong* arithmetically — its `avg` equals the `Rate`'s ratio — but it stores one float per iteration to reproduce a number two counters already hold, and the summary shows it as a bare mean rather than a percentage. Choose the type that matches the question, and the reporting follows for free. ## Putting it together 1. Import the two constructors from `k6/metrics`. 2. Build both at module scope, in the init context — the constructors throw anywhere else. 3. In the iteration, bracket the business step with timestamps and `add` the elapsed milliseconds to the trend. 4. `add` the validation boolean to the rate on every path, success and failure alike. The result is a report that says two independent things: how long the checkout step took at the tail, and what fraction of confirmations validated. Neither number could have been reconstructed later from the other metric type.

  • What does the second argument in new Trend('checkout_duration', true) change?
    It sets the metric's value type to time instead of the default. The statistics are unchanged — you still get `avg`, `min`, `max`, `med` and `p(N)` — but k6 now knows the numbers are millisecond durations and renders them in time units in the end-of-test summary rather than as plain floats.
  • A k6 Rate metric reports 100% on a run you know had failures. What is the likely bug?
    `.add()` is only being called on the success path. A `Rate` computes non-zero adds divided by total adds, so a failure that never reaches `.add(false)` never enters the denominator. Pass the boolean expression itself — `payloadValid.add(res.status === 200)` — so every iteration contributes exactly one sample.
  • Could a Trend replace the Rate and still give the pass share?
    Arithmetically yes: adding 1 and 0 to a `Trend` gives an `avg` equal to the `Rate`'s ratio. But the `Trend` stores one float per iteration where the `Rate` keeps two integers, and k6 renders a rate as a percentage while a trend shows a bare mean. Use `Rate` when the question is "what fraction".

saying these in an interview costs you the question

  • Uses a Counter for a pass rate and reports raw pass counts
  • Expects p(95) out of a Counter or a Gauge
  • Only calls Rate.add(true) on success, never on the failure path
  • Thinks a Gauge averages the durations recorded by all VUs
  • Believes the isTime flag changes which statistics a Trend reports
  • Records the whole step into a Gauge and reads its last value