Deep Dive8 min readMarch 12, 2025

k6 Metrics Explained: What to Watch and What to Ignore

k6 outputs dozens of metrics. Learn which ones actually matter, how percentiles work, and how to set meaningful thresholds.

By Perfscale Team

The metrics k6 always reports

Every k6 run produces these built-in metrics:

MetricTypeWhat it measures
http_req_durationTrendFull round-trip time from request sent to body received
http_req_waitingTrendTime to first byte (TTFB) — server processing time
http_req_sendingTrendTime to send the request body
http_req_receivingTrendTime to download the response body
http_req_failedRateFraction of requests that returned an error
http_reqsCounterTotal requests sent
vusGaugeActive virtual users right now
vus_maxGaugeMax VUs allocated during the test
iteration_durationTrendTime for one full VU iteration
data_receivedCounterTotal bytes downloaded
data_sentCounterTotal bytes uploaded

Trend metrics: never look at the average

http_req_duration is a Trend — k6 collects every sample and reports:

http_req_duration..............: avg=142ms min=89ms med=134ms max=2103ms p(90)=201ms p(99)=890ms

The average is misleading. A few very slow requests inflate it, masking the fact that 90% of users had a fast experience. Or the opposite — a bimodal distribution with half your users at 50ms and half at 2000ms averages out to 1025ms, hiding the problem entirely.

The percentiles that matter

PercentileWhat it tells you
p(50) / medianTypical experience
p(90)9 out of 10 users are faster than this
p(95)Common SLO boundary
p(99)The "tail" — your worst regular experience
p(99.9)Rare edge cases — only relevant at very high volume

Rule of thumb: Set your SLO on p95 or p99, not average.


Setting thresholds

Thresholds turn a load test into a pass/fail gate:

export const options = {
  thresholds: {
    // p99 latency under 500ms
    http_req_duration: ['p(99)<500'],

    // error rate under 1%
    http_req_failed: ['rate<0.01'],

    // at least 100 req/s throughput
    http_reqs: ['rate>100'],

    // only for the /api/checkout endpoint
    'http_req_duration{url:https://api.example.com/checkout}': ['p(95)<300'],
  },
};

Threshold syntax

metric_name[{tag:value,...}]: ['aggregation_function operator value']

Available aggregation functions: avg, min, max, med, p(n), count, rate, value


Custom metrics

You can define your own metrics and track business logic:

import { Trend, Counter, Rate, Gauge } from 'k6/metrics';

const checkoutLatency = new Trend('checkout_duration');
const cartErrors      = new Counter('cart_errors');
const loginSuccessRate = new Rate('login_success');

export default function () {
  const start = Date.now();
  const res = http.post('/api/checkout', payload);
  checkoutLatency.add(Date.now() - start);

  if (res.status !== 200) {
    cartErrors.add(1);
  }

  loginSuccessRate.add(res.status === 200);
}

Custom metrics appear in the output alongside built-in ones, and you can use them in thresholds too.


What to ignore

  • http_req_blocked — DNS lookup + TCP connect time. Only interesting if you're testing from a cold start or see values > 10ms consistently.
  • http_req_tls_handshaking — TLS setup time. Usually < 50ms and amortised across keep-alive connections.
  • data_received / data_sent — Useful for capacity planning, not SLO validation.

The one metric dashboard you need

In Grafana, visualise:

  1. RPS (http_reqs rate) — is the load hitting the target?
  2. p99 latency (http_req_duration p99) — is the service staying fast?
  3. Error rate (http_req_failed rate) — is anything breaking?
  4. Active VUs (vus) — confirms the ramp-up is working

These four panels tell you everything you need to make a pass/fail decision.