skip to content

Custom Counters and Trends

Declaring your own metric from k6/metrics in the init context and feeding it values per iteration. Interviewers probe it because the type you pick fixes which statistics ever exist.

on this pageshow

explore

questions

4

Which four metric classes does k6's k6/metrics module export, and what does each keep?

level: juniorimportance: must knowfreq 76%

answer

  1. four constructors, no fifth
  2. sum, last value, share, every value
  3. only one type keeps each sample
  4. Trend alone can produce p(N)

basics

~20 s

k6/metrics exports Counter, Gauge, Rate and Trend. Counter sums added values and reports count and rate; Gauge keeps the last value; Rate reports the share of non-zero values; Trend keeps every value, giving avg, min, max, med and p(N).

solid answer

~40 s

In k6 v2 the `k6/metrics` module exports exactly four constructors: `Counter`, `Gauge`, `Rate` and `Trend`. A `Counter` accumulates a running total and reports `count` plus a per-second `rate`. A `Gauge` overwrites itself on every add, so only the last value survives, reported as `value`. A `Rate` counts each added value and counts it again as a "true" if it is non-zero, reporting `rate` as a share between 0.00 and 1.00 (rendered as a percentage in the end-of-test summary). A `Trend` appends every value to a list, which is why it is the only type that can report `avg`, `min`, `max`, `med` and `p(N)`. All four are built in the init context and fed with `.add(value, tags)` from VU code.

code

javascript · 13 lines
javascript
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';

const couponsRedeemed = new Counter('coupons_redeemed');
const queueDepth = new Gauge('queue_depth');
const payloadValid = new Rate('payload_valid');
const checkoutDuration = new Trend('checkout_duration', true);

export default function () {
  couponsRedeemed.add(1);
  queueDepth.add(17);
  payloadValid.add(true);
  checkoutDuration.add(842);
}

go deeper

for a junior

Recall the four names - Counter, Gauge, Rate, Trend - and one sentence each on what they keep. At a screening that is the whole expected answer.

for a middle

Explain the sink behind each type: a running sum, an overwritten slot, a trues-over-total ratio, a list of every value. That is what makes p(N) possible only on a Trend.

for a senior

Show that you pick the type from the sentence the report has to say, because a wrong choice cannot be repaired later - the discarded observations are gone.

for a principal

Weigh Trend's per-value retention against Counter and Rate, which collapse to a couple of numbers, when a long run has to stay cheap to record, ship and store.

## What `k6/metrics` is for k6 emits its own built-in measurements for HTTP, iterations and VUs, but anything specific to **your** application has to be measured by you: how long a checkout step took end to end, how often a response body passed validation, how many coupons a run redeemed. `k6/metrics` is the module for that, and in k6 v2 it exports exactly four constructors and nothing else — **`Counter`**, **`Gauge`**, **`Rate`** and **`Trend`**. There is no fifth type, no histogram class, and no way to change a metric's type once it has been created. Each constructor is called in the init context (at module scope, outside any exported function) and returns a small object with just two members: a read-only **`name`** property, and an **`add(value, [tags])`** method you call from VU code. That is the entire surface. ```javascript import { Counter, Gauge, Rate, Trend } from 'k6/metrics'; const couponsRedeemed = new Counter('coupons_redeemed'); const queueDepth = new Gauge('queue_depth'); const payloadValid = new Rate('payload_valid'); const checkoutDuration = new Trend('checkout_duration', true); ``` ## The four types side by side | Type | What it does with each added value | Statistics k6 reports | |---|---|---| | `Counter` | adds the value to a running total | `count`, and `rate` = total divided by elapsed seconds | | `Gauge` | replaces the stored value | `value`, the last one added | | `Rate` | counts the sample, and counts it again if non-zero | `rate` = non-zero adds divided by total adds, 0.00–1.00 | | `Trend` | appends the value to the list of all values seen | `avg`, `min`, `max`, `med`, `p(N)` | ## What each one is actually for - **`Counter` — "how many" or "how much in total".** It is monotonic within a run: every `add()` moves the total up by the value passed. `add(1)` per event turns it into an event counter; `add(bytes)` turns it into a volume counter. - **`Counter`'s second statistic is throughput, not a share.** Its `rate` is the accumulated total divided by the run's elapsed seconds, so it answers "per second", never "out of how many". - **`Gauge` — "what is it right now".** Every `add()` clobbers the previous value; k6 reports only `value`. It suits a level that has a current reading (a queue depth, a response body size on the last request) and is a poor fit for anything you want to compare across iterations. - **`Rate` — "what fraction".** Internally it holds two counters: total adds and non-zero adds. `add(true)` and `add(1)` increment both; `add(false)` and `add(0)` increment only the total. The reported `rate` is the ratio, which is why an add on the failure path matters as much as one on the success path. - **`Trend` — "give me statistics over every value".** It is the only type that retains individual values, so it is the only one from which an average, a median, a maximum or a percentile can be computed at all. `med` is exactly `p(0.5)`, and percentiles between two stored values are linearly interpolated. - **A `Gauge` is the one type where arrival order decides the answer.** With many VUs writing into it, `value` is whatever sample landed last, so it only means something for a level that really is global rather than per-iteration. - **All four are fed the same way.** `add(value, [tags])` is the entire input surface — there is no `set()`, no `observe()`, and no way to reach into a metric and read its accumulated statistics from the script. ## Construction and naming rules 1. **Construct in the init context.** `new Trend('checkout_duration')` inside the default function throws `metrics must be declared in the init context`. 2. **Names are validated.** A metric name must match `^[a-zA-Z_][a-zA-Z0-9_]{1,128}$` — ASCII letters, digits and underscores, starting with a letter or an underscore. `checkout-duration` is rejected at construction time because of the hyphen. 3. **An optional second argument marks the values as time.** `new Trend('checkout_duration', true)` sets the metric's value type to time, so k6 renders the numbers as millisecond durations in the summary instead of bare floats. It does **not** change which statistics exist. ## Why the choice is effectively permanent A metric's type decides which sink k6 builds behind it, and the sink decides which numbers ever exist. A `Counter` never stores an individual observation, so no amount of post-processing can recover a percentile from it — the information was discarded at `add()` time. A `Gauge` throws away every value but the last. A `Rate` reduces everything to two integers. Only `Trend` keeps enough to answer a distribution question later. The corollary runs the other way too: `Trend` is the expensive type, because it holds one float per recorded value for the life of the run. When all you need is "how many" or "what fraction", `Counter` and `Rate` collapse the data to a couple of numbers and stay flat no matter how long the run lasts. So the practical discipline is to write down the sentence you want the report to say first — "the checkout step's `p(95)` was 840 ms", "94% of payloads validated", "the run redeemed 1,203 coupons" — and let that sentence pick the type. Getting it wrong is not a formatting problem you can fix afterwards; it is data you did not record.

  • What does a k6 Counter's rate mean, and how does it differ from a Rate metric's rate?
    A `Counter`'s `rate` is throughput: its accumulated total divided by the run's elapsed seconds, so it is a per-second figure. A `Rate` metric's `rate` is a proportion: non-zero adds divided by total adds, always between 0.00 and 1.00. Same word, unrelated quantities — which is why reading a summary line without knowing the metric's type is guesswork.
  • Does a k6 Gauge remember the highest value it ever saw?
    Not in what it reports. `Gauge.add()` overwrites the stored value on every call, and the only statistic k6 reports for a gauge is `value`, the last one added. If you need the peak across a run, record the values into a `Trend` and read its `max` instead.
  • Why can a Trend report a percentile when a Counter cannot?
    Because of what each one stores. A `Trend` appends every value to a list, so k6 can sort it and interpolate any `p(N)` at the end of the run. A `Counter` only ever holds a running sum, so the individual observations no longer exist by the time the summary is built — there is nothing left to take a percentile of.

saying these in an interview costs you the question

  • Claims k6 has a Histogram or Summary custom metric type
  • Expects a percentile such as p(95) from a Counter
  • Believes a Gauge averages the values added to it
  • Reads a Rate's rate as events per second
  • Thinks a metric's type can be changed after it is created
open as a page

In k6, what happens if you construct a k6/metrics Trend inside the default function instead of at module scope?

level: middleimportance: must knowfreq 64%

basics

~10 s

Constructing a k6 custom metric outside the init context throws "metrics must be declared in the init context". Only .add() may run inside an iteration; the k6/metrics constructors must sit at module scope.

open as a page

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%

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.

open as a page

In k6, why might a custom metric's .add() call record nothing and leave only a warning in the log?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Because k6's .add() accepts only numbers and booleans. undefined, null, NaN, a non-numeric string or an object is dropped with an "is an invalid value for metric" warning and add() returns false - an error only if options.throw is set.

open as a page