In k6, what does a threshold key like http_req_duration{status:200} do that a plain metric name does not?
answer
- braces are a filter, not a label
- a second metric is registered
- selector must be a subset of the tags
- comma means AND, never OR
- sub-metric inherits the parent's type
basics
~10 sThe braces create a sub-metric: k6 registers a filtered copy of http_req_duration that receives only the samples whose tags contain status:200, and evaluates the threshold against that copy instead of the whole metric.
solid answer
~30 sIn k6 v2 a `thresholds` key can be a metric expression rather than a bare metric name. `http_req_duration{status:200}` makes k6 register a **sub-metric** before the test starts: a clone of `http_req_duration` with the same type that only receives samples whose indexed tags contain the pair `status:200`. The threshold expression is then evaluated against that sub-metric's sink, so `p(95)<500` describes the tagged slice, not the whole run. Several pairs separated by commas are combined with AND, matching is exact string equality on a subset basis, and both the sub-metric and its parent show up in the end-of-test summary.
code
javascript · 18 linesimport http from 'k6/http';
export const options = {
vus: 5,
duration: '30s',
thresholds: {
// the whole metric
http_req_duration: ['p(95)<800'],
// only the samples tagged endpoint:checkout
'http_req_duration{endpoint:checkout}': ['p(95)<500'],
},
};
export default function () {
http.get('https://quickpizza.grafana.com/api/json', {
tags: { endpoint: 'checkout' },
});
}go deeper
Recall the shape: metric name, then braces holding key:value pairs, as the key of an entry in the k6 thresholds object. Know that it narrows the rule to the matching samples.
Explain that k6 registers a real second metric of the parent's type and feeds it only samples whose tags contain every pair, and that commas are AND while values compare as exact strings.
Show you know the sub-metric is created at load time, before any VU runs, that it and its parent both appear in the summary, and what that implies when the tag turns out never to be applied.
Frame the cost side: each scoped key adds a metric and a time series to keep, and a suite of narrow selectors is only as reliable as the tag literals it depends on staying in sync with the requests.
## What a braced threshold key means In k6 v2 the keys of the exported `thresholds` object are metric **expressions**, not plain metric names. A key shaped `metric_name{tag_key:tag_value}` asks k6 to build a **sub-metric**: a second metric, registered under the whole string including the braces, that inherits its parent's type and value type but is fed only a filtered slice of the parent's samples. `http_req_duration{endpoint:checkout}` is therefore a Trend, exactly like `http_req_duration` itself, and its sink only ever sees durations from samples whose `endpoint` tag was the string `checkout`. The expression on the right-hand side of the key is evaluated against that filtered sink and never against the parent. ## When k6 builds the sub-metric Creation happens while the test is loading, before the first VU starts: 1. k6 consolidates the config layers, then validates every `thresholds` key. It splits the key at the first `{` and looks up the text in front of the brace in the metric registry. A name that no built-in metric and no metric declared in the init context owns is an invalid configuration, and the run never starts. 2. The metrics engine walks the same map again and builds the sub-metric from the text between the braces, splitting it on commas and splitting each pair on its first colon. 3. Both the sub-metric and its parent are marked *observed* at that moment. That is why a scoped row appears in the end-of-test summary even when it never records a value. ## How a sample reaches a sub-metric Every sample k6 emits carries an indexed tag set. The internal ingester flushes buffered samples roughly every 50 ms; for each sample it adds the value to the parent metric's sink, then walks the parent's list of sub-metrics and adds the sample to any sub-metric whose tag set is a **subset** of the sample's tags. Four rules follow directly from that word *subset*: - **Extra tags never disqualify a sample.** `{status:200}` matches a sample tagged `status=200, method=GET, name=https://example.com/cart`. The selector only has to be contained in the sample's tags, not equal to them. - **A comma means AND.** `http_req_duration{status:200,method:GET}` selects only samples carrying both pairs. There is no OR form, no wildcard and no regular expression. - **Matching is exact string equality.** Tag values are strings — `status` is the response code rendered as text, and `0` when the request never received a response — so `{status:200}` never matches `201` or `204`. - **Pair order is irrelevant.** k6 stores tag sets in a canonically ordered structure, so `{a:1,b:2}` and `{b:2,a:1}` are the same selector. Listing both as separate threshold keys resolves to one sub-metric, and one of the two rule lists silently replaces the other. ## Spelling rules the key parser applies Whitespace around a key or a value is trimmed and one layer of single or double quotes is stripped, so `{ status : "200" }` and `{status:200}` build an identical tag set. Only the first colon in a pair splits it, so a value may itself contain colons, slashes and braces: `group_duration{group:::checkout}` names the group whose full path is `::checkout`, and `http_req_duration{name:http://example.com/cart}` is accepted. A value can never contain a comma, because the comma split runs first. Repeating a key is not a union either — `{status:200,status:201}` collapses to one `status` tag holding `201`. ## Scoped against unscoped, side by side | aspect | `http_req_duration` | `http_req_duration{endpoint:checkout}` | |---|---|---| | what feeds it | every sample of the metric | only samples whose tags contain that pair | | where it comes from | registered by k6 at startup | created from the threshold key at startup | | what emptiness means | no request was made at all | the tag was never applied, or no request was made | | shown in the summary | always | always, because the threshold key marked it observed | | type and legal aggregations | its own | inherited from the parent metric | The last two rows are why scoped rules deserve care. k6 will create, display and pass a rule over a slice that received no samples at all, and it validates the aggregation you wrote against the *parent* metric's type, because the sub-metric is a clone of it. A scoped threshold is therefore a filter over a stream, evaluated at the end like any other rule — powerful for asserting that one endpoint, one status class or one group is fast, and quiet in exactly the case where the filter turns out to select nothing.
- Can one k6 threshold key select two status codes, such as 200 or 201?No. Commas inside the braces are combined with AND, and repeating a key keeps only the last value, so `{status:200,status:201}` selects `201` alone. To cover both, tag the requests yourself with a shared value and scope on that tag, or declare two separate threshold keys.
- Does the sub-metric have its own metric type, or the parent's?The parent's. k6 clones the parent's type and value type when it creates the sub-metric, so `http_req_duration{...}` is a Trend and `http_reqs{...}` is a Counter. That is also the type k6 validates your aggregation against when it checks the threshold key before the run.
- Does a sample with tags beyond those in the selector still match?Yes. k6 tests whether the selector's tag set is a subset of the sample's tag set, so a sample tagged status=200, method=GET and name=... matches `{status:200}`. Only the pairs you name have to be present, with exactly those string values.
saying these in an interview costs you the question
- Thinking the braces only relabel the metric row in the summary
- Reading a comma inside the braces as OR instead of AND
- Expecting wildcards or a regular expression in the tag value
- Assuming the sample's tags must equal the selector exactly
- Believing the selector filters the parent metric in place