skip to content

Which extra metrics does a k6 browser scenario emit, and why is http_req_duration not one of them?

level: middleimportance: should knowfreq 47%

answer

  1. a second, prefixed family
  2. five vitals, four transport metrics
  3. one is a score, not a time
  4. FID is gone in v2

basics

~10 s

A browser scenario adds nine metrics: five web vitals (browser_web_vital_lcp, fcp, ttfb, inp, cls) plus browser_data_sent, browser_data_received, browser_http_req_duration and browser_http_req_failed. Chromium issues the page traffic, so the core http_req_* metrics never see it.

solid answer

~30 s

Enabling the browser in a k6 scenario registers a second metric family, all prefixed `browser_`. Five of them are web vitals — `browser_web_vital_lcp`, `browser_web_vital_fcp`, `browser_web_vital_ttfb`, `browser_web_vital_inp` and `browser_web_vital_cls` — and four cover the browser's own transport: `browser_data_sent`, `browser_data_received`, `browser_http_req_duration` and `browser_http_req_failed`. They exist separately because the requests are made by Chromium and reported to k6 over the DevTools protocol, not issued by k6's own HTTP client, so `http_req_duration`, `http_reqs` and `data_received` count only what `k6/http` sent. In a script with both a browser scenario and a protocol scenario, the summary shows both families side by side in separate sections.

code

javascript · 15 lines
javascript
export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: { type: 'chromium' },
      },
    },
  },
  thresholds: {
    browser_web_vital_lcp: ['p(95) < 2000'],
    browser_web_vital_fcp: ['p(95) < 1000'],
    http_req_duration: ['p(95) < 500'],
  },
};

go deeper

for a junior

Recall that browser work reports under its own browser_ prefixed metrics, and that browser_web_vital_lcp is the headline one. Knowing they are separate from http_req_duration is the key point.

for a middle

List the two groups — five web vitals and four transport metrics — and explain the separation: Chromium issues the requests and reports them over the DevTools protocol, so k6's own HTTP client never sees them.

for a senior

Show you can read a mixed run: which threshold judges which scenario, why http_reqs can legitimately be zero, and how per-URL tagging on browser metrics turns into a series-count problem on a real site.

for a principal

Decide what the organisation standardises on: whether browser and protocol work share one k6 script and one summary, or run as separate suites with separate outputs, and who owns each metric family.

## Two metric families, one run k6's built-in metrics are registered by k6 core; the `browser_` metrics are registered by the browser module when it loads. That is the whole reason they are a distinct family: they only exist when a run actually drives a browser, and they measure something k6's own HTTP client never touched. There are nine of them in k6 v2 — five web vitals and four transport metrics. ## The five web vitals All five are **Trend** metrics, so a run reports the usual spread of statistics for each. Four of them hold durations; one does not. | Metric | Holds | Measures | |---|---|---| | `browser_web_vital_lcp` | time | Largest Contentful Paint — when the largest content element becomes visible | | `browser_web_vital_fcp` | time | First Contentful Paint — when the first DOM element is rendered | | `browser_web_vital_ttfb` | time | Time to First Byte — request to the start of the response | | `browser_web_vital_inp` | time | Interaction to Next Paint — a page's responsiveness to interaction | | `browser_web_vital_cls` | unitless score | Cumulative Layout Shift — how much visible content moved unexpectedly | `browser_web_vital_cls` is the one to remember, because it is registered with the default value type rather than the time type: it is a score, not a duration, so a summary prints it as a bare number and a threshold on it is compared against a number rather than a millisecond count. **There is no `browser_web_vital_fid` in k6 v2.** First Input Delay was dropped when the bundled web-vitals library was updated to v5.1.0 for the v2.0.0 release, following the upstream deprecation of FID in favour of INP. Older k6 output samples still circulating on the web show an FID line; a script that references it in k6 v2 is referencing a metric no run will ever produce. ## The four transport metrics These describe what the browser put on the wire, and they mirror the names of the core metrics deliberately: - **`browser_data_sent`** and **`browser_data_received`** — counters of bytes, the browser's equivalent of `data_sent` and `data_received`. - **`browser_http_req_duration`** — a trend over the duration of each request the page made, including every subresource: images, stylesheets, fonts, XHR. - **`browser_http_req_failed`** — a rate of failed browser requests. One page navigation typically produces dozens of samples in these metrics, because a rendered page fetches far more than the document itself. ## Why the core HTTP metrics stay empty The split is architectural, not cosmetic. `k6/http` requests are issued by k6's own Go HTTP client, which is what feeds `http_reqs`, `http_req_duration`, `http_req_failed` and the `data_*` counters. Browser requests are issued by the Chromium process; the module learns about them by listening to Chrome DevTools Protocol events and records them into the `browser_*` metrics. Nothing crosses over in either direction. The consequence bites in a mixed script: 1. The `http_req_duration` line describes only the protocol scenario, no matter how slow the pages were. 2. A `browser_web_vital_*` or `browser_http_req_*` line describes only the browser scenario. 3. If a run has a browser scenario and no `k6/http` calls at all, `http_reqs` is zero — that is correct output, not a broken run. k6's end-of-test summary reflects the split by grouping metrics into sections, with the browser and web-vital metrics reported apart from the HTTP ones, so the two families are visually separated before you even read the names. ## Using them The `browser_*` metrics behave like any other k6 metric once they exist: they are tagged, they can carry thresholds, and they flow to whatever output the run is configured with. Web vitals are also recorded per page URL, so a script that navigates several pages produces samples distinguishable by their `url` tag — which is exactly why a script that visits many distinct query-string URLs can generate a large number of time series unless those URLs are grouped under a common name. A few practical notes: - Web vitals are only collected while a page is open and settled; a page that is never closed can distort them. - `browser_web_vital_inp` needs an actual interaction to have a value — a script that only navigates may report zero samples for it. - Because the browser metrics are registered by the module, a run with no browser scenario shows none of them at all.

  • A browser run reports http_reqs of 0. Is the run broken?
    No. `http_reqs` counts requests made through `k6/http`. A pure browser scenario makes none, so zero is the correct value; the page traffic is in `browser_http_req_duration` and the `browser_data_*` counters instead.
  • Why is browser_web_vital_cls printed as a plain number rather than a duration?
    Because it is registered with the default value type instead of the time type. Cumulative Layout Shift is a unitless score describing how much visible content moved, so k6 stores and prints it as a number.
  • A script visits URLs with unique query strings and the output series count explodes. Why?
    Browser metrics carry the page URL as a tag, so every distinct URL becomes its own series. The browser module lets you rename matching request URLs to a shared name from a page metric handler, which collapses them back into one series.

saying these in an interview costs you the question

  • Expects page requests to appear in http_req_duration
  • Names browser_web_vital_fid as a current k6 v2 metric
  • Treats browser_web_vital_cls as a duration in milliseconds
  • Thinks the browser metrics exist in every k6 run
  • Assumes one navigation produces one browser request sample