skip to content

In a k6 suite, when do you use group(), a name tag, or options.tags to carry a dimension?

level: principalimportance: should knowfreq 40%

answer

  1. scope decides the mechanism
  2. run identity versus operation identity
  3. containment versus per-call identity
  4. one group per request buys nothing

basics

~20 s

Match the scope: options.tags for run identity, a name tag for operation identity, group() for a multi-step stretch, exec.vu.tags for a runtime variant. A group around a single request duplicates name and adds group_duration for nothing.

solid answer

~40 s

Match the mechanism to the scope of the dimension. `options.tags` — or `--tag` / `K6_TAGS` from the pipeline — carries run identity such as build, branch or environment: one constant value across every sample, costing nothing in extra series. A `name` tag carries operation identity and is the dimension you slice by most often; k6 already fills it from the URL, so overriding it is how you collapse a parameterised route. `group()` carries journey structure: it tags everything inside the callback with a `::`-joined path and emits a `group_duration` sample, so it earns its keep only for a genuinely multi-step stretch. `exec.vu.tags` carries a runtime variant spanning several calls. The anti-pattern k6's own docs name is a group around a single request, which merely repeats `name` and adds a metric.

code

javascript · 18 lines
javascript
import http from 'k6/http';
import { group } from 'k6';

export const options = {
  tags: { build: __ENV.BUILD_ID || 'local' }, // run identity
  vus: 1,
  iterations: 1,
};

export default function () {
  group('browse', function () {
    // operation identity, collapsed by the template
    http.get(http.url`https://quickpizza.grafana.com/api/json?id=${Math.ceil(Math.random() * 50)}`);
    http.get('https://quickpizza.grafana.com/api/headers', {
      tags: { name: 'Headers' },
    });
  });
}

go deeper

for a junior

Know the vocabulary: options.tags covers the whole run, a name tag identifies one request, and group() covers everything inside its callback.

for a middle

Be able to say what each mechanism costs - a run tag adds no new values, a name or group value adds a series family per distinct value, and a group also adds a metric.

for a senior

Recognise the one-group-per-request smell on sight, and be able to say what should replace it: a name tag on the request, and a group only where the unit is several steps.

for a principal

Own the scheme: which dimensions the suite standardises on, one spelling per key, run identity supplied from the pipeline, and a series budget checked against k6's 100,000-series warning.

## The four mechanisms, and what each one's shape is k6 v2 gives you four ways to put a dimension onto metric samples, and they differ in **scope** — how much of the run one value covers — far more than in effort. | Mechanism | Scope of one value | Extra metric emitted | Cost per distinct value | | --- | --- | --- | --- | | `options.tags` / `--tag` / `K6_TAGS` | every sample in the run | none | one value for the whole run | | `tags: { name: … }` on a request | that request's HTTP samples | none | one series family per name | | `group('x', fn)` | everything inside the callback | `group_duration` | one series family per path, plus the metric | | `exec.vu.tags.x = …` | from the assignment until deleted | none | one series family per value | Reading the table as a decision rule: a run tag answers "which run is this?", a `name` tag answers "which operation is this?", a group answers "which part of the journey is this?", and `exec.vu.tags` answers "which variant did this VU take?". ## Where each one is the right answer - **`options.tags` for run identity.** Build number, branch, environment, load profile label. The value is constant for the run, so it adds no series multiplication at all — one value across every sample. Set it from the pipeline with `--tag` or `K6_TAGS` rather than hardcoding it in the script. - **`name` for operation identity.** This is the dimension you will actually slice by most often, and it is the one k6 already populates for you from the URL. Overriding it is how you collapse a parameterised route into one operation, and k6 mirrors your value onto `url` so you do not pay twice. - **`group()` for journey structure.** A group is a *containment* dimension: it applies to everything inside, including checks and custom metrics, and it nests. Use it when the meaningful unit is a multi-step stretch — "browse", "add to cart", "checkout" — not a single call. - **`exec.vu.tags` for runtime variants.** A cohort, a data-file segment, a feature-flag branch: known only once the iteration starts, spanning several calls, and not expressible as a static argument. ## The mistake worth naming Wrapping each individual request in its own `group()` is the pattern k6's own documentation calls discouraged, and it is the one most k6 suites drift into. It buys nothing: the `name` tag already identifies that request, so the group tag is a second copy of the same dimension, and each call adds a `group_duration` sample that duplicates `http_req_duration` for a single-request group. It also inflates the tag combination count, since `group` and `name` now vary together. The signal to look for is a group whose body is one call. If the group exists only to give the request a readable label, the label belongs in `tags: { name: … }`. ## The judgment calls that actually have no single answer 1. **How deep to nest groups.** Nesting is free syntactically, and each level appends to the `::` path, so a three-deep tree gives you a distinct tag value per leaf. The question is whether anyone will ever filter at that depth. A defensible default is one level for the user journey and a second only where a stretch is long enough to want its own `group_duration`. 2. **Whether a dimension belongs on the run or on the sample.** Anything constant for the run — the environment, the release — is cheaper and clearer as `options.tags` than as a per-request tag, because it does not multiply against anything. Anything that varies within the run has to be a per-entity tag, and then the question becomes how many values it can take. 3. **How much of the default `systemTags` set to keep.** Trimming it is the only lever that removes dimensions rather than adding them, and it is irreversible for that run's results. 4. **Where the ceiling is.** k6 warns once a run generates more than 100,000 unique time series, and that warning is the concrete budget a tagging scheme has to live inside. Multiply your planned dimensions together before the run, not after. ## What to standardise across a k6 suite - One spelling per dimension. A sub-metric selector such as `http_req_duration{name:Checkout}` matches an exact string, and k6 will not tell you that another script wrote `checkout`. - A fixed `name` label per endpoint, decided once and reused, so the same operation is one series wherever it appears. - Run-identity tags supplied only from the pipeline, so a script never disagrees with the run it is in. - A rule that `group()` never wraps a single call — the cheapest review comment on this whole subject.

  • Why is one group() per request discouraged in k6?
    The `name` tag already identifies that request, so the group tag is a second copy of the same dimension, and each call adds a `group_duration` sample that mirrors the request's own duration. Extra metric, extra tag values, no new information.
  • Which k6 tagging mechanism costs nothing in extra time series?
    A test-wide tag from `options.tags`, `--tag` or `K6_TAGS`. Its value is constant for the run, so it multiplies nothing. Every per-entity mechanism adds a value that combines with each other tag on the sample.
  • What concrete budget does k6 give a tagging scheme?
    k6 warns once a run generates more than 100,000 unique time series, doubling the threshold each time it fires. Multiplying your planned dimensions together before the run is how you stay inside it.
  • Where should run-identity tags actually be set?
    From the pipeline, with `--tag` or `K6_TAGS`, rather than hardcoded as `options.tags` in the script. The script then cannot disagree with the run it is part of, and the same file works unchanged in every environment.

saying these in an interview costs you the question

  • Wraps every request in its own group as a matter of style
  • Puts run identity on each request instead of options.tags
  • Treats the group tag and the name tag as interchangeable
  • Nests groups four deep with nobody filtering at that depth
  • Adds tag keys without ever counting the combinations