Module 3: Metrics Latency Throughput Errors
6. Reading the `k6 run` summary
Overview
You already have the three instruments —latency, throughput, and errors— and you know how to compute them with your own hands. This lesson brings the pieces together and teaches you to read them in their native format: the end-of-test summary k6 prints when a run finishes. It's the screen you'll see every time you run k6 run, and knowing how to walk through it line by line —what each row measures, where the p95 is, where the error rate, where the RPS— is the practical skill this whole module was building toward. By the end, that block of numbers will stop being intimidating and become a dashboard you read at a glance.
Some honesty up front, the usual in this guide: k6 isn't installed in this environment (it's a Go binary with its own runtime, and neither node nor python runs it). So all the k6 output blocks in this lesson go as labeled content: they're faithful to k6's official format —I verified them against its documentation— but they were not run here, and I never present them to you as if they had been. The nice thing is that you don't need to run k6 to learn to read its summary, because every number that appears in it you already computed yourself in Python in the previous lessons. This lesson's last section does precisely that bridge: mapping each line of the k6 summary to the metric you produced yourself.
Connection to the module: this lesson is the synthesis. Lessons 2-3 gave you the latency and the percentiles (which you here read in http_req_duration); lesson 4, the throughput (http_reqs, iterations); lesson 5, the error rate (http_req_failed). Here you see all three on a single screen. The k6 script that produces this summary is the anatomy of module 2, which we reuse without re-explaining. And the thresholds that will appear above the summary (the THRESHOLDS section) are a preview of module 5: here we only name them; making them gate the deploy is over there.
The detailed supermarket receipt
When you finish the big monthly shop, the register gives you a long receipt. It's not a single figure: it's organized into sections —dairy, fruit, cleaning— and at the end, the totals: subtotal, taxes, total to pay. If you know how to read it, at a glance you see how much you spent in each category and where your money went. If you don't, it's a scary column of numbers. Learning to read the receipt isn't learning to add —you already know that—; it's learning where each thing is and what each line means.
The k6 run summary is that receipt. It's organized into labeled sections (HTTP, EXECUTION, NETWORK) and, at the very top, the "totals" that matter most if you set thresholds (THRESHOLDS). Each line is a metric with its statistical summary. The skill this lesson installs isn't computing anything —you already know— but navigating the receipt: knowing that the latency lives in the HTTP section under http_req_duration, that the p95 is the p(95) column, that the error rate is http_req_failed, that the RPS is the /s rate of http_reqs. Once you know where to look, the summary reads in ten seconds.
The script that produces the summary (context, from module 2)
For context, this is the kind of k6 script that generates a summary like the ones below. Its anatomy —the default function, http.post, check, sleep, options— is from module 2; we include it only so you know where the numbers come from, not to re-explain it.
// load-test.js — CONTENT (k6 is not installed; this is how a k6 script looks)
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50, // 50 virtual users
duration: '60s', // for 60 seconds
};
export default function () {
const url = 'http://127.0.0.1:8080/quote';
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(url, payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
'price is 7500': (r) => r.json('price_cents') === 7500,
});
sleep(1); // think time: the VU "thinks" 1 s between iterations
}
The k6 run summary, line by line
When you run k6 run load-test.js, k6 first prints a header with the run's configuration and then, on finishing, the summary. This is what that output looks like —labeled content, faithful to k6's official format, not run here—:
# CONTENT (this is how `k6 run` looks; k6 is not installed in this environment)
execution: local
script: load-test.js
output: -
scenarios: (100.00%) 1 scenario, 50 max VUs, 1m30s max duration (incl. graceful stop):
* default: 50 looping VUs for 1m0s (gracefulStop: 30s)
█ TOTAL RESULTS
checks_total.......................: 2940 48.99/s
checks_succeeded...................: 100.00% 5880 out of 5880
checks_failed......................: 0.00% 0 out of 5880
✓ status is 200
✓ price is 7500
HTTP
http_req_duration..................: avg=13.2ms min=0.9ms med=6.1ms max=254ms p(90)=39ms p(95)=183ms
{ expected_response:true }.......: avg=13.2ms min=0.9ms med=6.1ms max=254ms p(90)=39ms p(95)=183ms
http_req_failed....................: 0.00% 0 out of 2940
http_reqs..........................: 2940 48.99/s
EXECUTION
iteration_duration.................: avg=1.01s min=1.0s med=1.0s max=1.25s p(90)=1.04s p(95)=1.18s
iterations.........................: 2940 48.99/s
vus................................: 50 min=50 max=50
vus_max............................: 50 min=50 max=50
NETWORK
data_received......................: 388 kB 6.5 kB/s
data_sent..........................: 511 kB 8.5 kB/s
Now walk through it by sections, which is how it's read.
The header (execution / scenarios). Before the summary, k6 reminds you what ran: 1 scenario, 50 max VUs, for 1 minute. It's the context without which the metrics mean nothing —"183 ms p95" only makes sense if you know it was with 50 VUs—.
TOTAL RESULTS → the checks. The first three lines (checks_total, checks_succeeded, checks_failed) summarize the script's check()s: here, 5880 verifications (two per iteration: "status is 200" and "price is 7500"), all successful. The checks verify correctness under load (that the price stays 7500 even with 50 VUs) and are the subject of module 6; here just recognize where they appear.
HTTP section → latency, errors, throughput. It's the heart of the receipt, and where your three instruments live:
| Summary line | What it is | From which lesson |
|---|---|---|
http_req_duration | The latency, with avg/min/med/max/p(90)/p(95). The p95 (p(95)=183ms) is your tail metric. | Lessons 2-3 |
{ expected_response:true } | The same latency, but only of the requests k6 considers successful. Useful for not polluting the percentile with the errors' latency. | Lesson 5 |
http_req_failed | The error rate (0.00% 0 out of 2940): percentage and count of failed requests. | Lesson 5 |
http_reqs | The throughput: total (2940) and rate (48.99/s, the RPS). | Lesson 4 |
EXECUTION section → iterations and VUs. iterations counts the script executions (here it coincides with http_reqs because each iteration makes one request); iteration_duration measures how long each complete iteration took (notice: ~1.01 s, dominated by the sleep(1) think time). vus and vus_max confirm how many virtual users were active (50). Note how the header, iterations, and http_reqs tell the same throughput story from three angles.
NETWORK section → data. data_received and data_sent: how many bytes the test moved, as total and as rate. It's rarely your main metric, but it betrays abnormally large responses (if data_received explodes, maybe you're downloading huge payloads, and that inflates http_req_receiving).
The bridge: each k6 line is something you already computed
Here's the reason you don't need to run k6 to understand its summary: each line corresponds exactly to a metric you produced in Python in the previous lessons. Notice the equivalence:
| k6 summary line | What you computed in Python |
|---|---|
http_req_duration ... p(95)=183ms | statistics.quantiles(latencies, n=100)[94] → your p95 (lesson 3) |
http_req_duration ... med=6.1ms | statistics.median(latencies) or your p50 |
http_req_duration ... avg=13.2ms | statistics.fmean(latencies) → the average (which you know lies if there's a tail) |
http_req_failed ... 0.00% | errors / total → your error rate (lesson 5) |
http_reqs ... 48.99/s | total / wall_time → your RPS (lesson 4) |
iterations | the number of tasks your generator launched |
vus_max | the max_workers of your ThreadPoolExecutor (your "VUs") |
Put another way: k6 doesn't do statistical magic you can't reproduce. It does the same as your Python generator —launch VUs, measure latencies, count errors, compute percentiles— but industrialized (more VUs, more protocols, more precision) and with a standardized output. That you computed the p95 with statistics.quantiles is exactly what lets you look at p(95)=183ms in the k6 receipt and know, without hesitation, what it means and where it came from.
A note on versions and formats
The summary's format has changed a bit between k6 versions. The one you see above —with TOTAL RESULTS, HTTP, EXECUTION, NETWORK sections and checks_succeeded/checks_failed— is the recent k6 versions' one. Older versions showed the same metrics with a slightly different format (for example, checks.....: 100.00% ✓ 5880 ✗ 0). The metric names don't change (http_req_duration, http_req_failed, http_reqs are stable); only the presentation changes. If your summary looks somewhat different from the one here, look for the same metrics by their name: they're still there.
Common mistakes
Reading the avg of http_req_duration as "the latency." What happens: someone looks at avg=13.2ms in the receipt and reports "the API runs at 13 ms," ignoring that on the same line p(95)=183ms. Why it happens: the avg is the first column and the most familiar. How to detect it: if your reported latency is the avg and not the p(95), you made the mistake from lessons 2-3 inside the k6 summary. How to fix it: in http_req_duration, read the p(95) (and the med), not the avg. The summary gives you both precisely so you compare and see the tail.
Ignoring the scenarios header and comparing incomparable runs. What happens: someone compares the p95 of a 10-VU run with that of a 200-VU run and concludes "the latency got worse," when what changed was the load. Why it happens: they skip the header and only look at the summary. How to detect it: if you compare two summaries without verifying that the scenarios section (VUs, duration) is the same, you compare apples to oranges. How to fix it: always read the header first; a p95 is only comparable to another measured under the same load.
Confusing checks with http_req_failed. What happens: someone sees checks_succeeded: 100.00% and thinks there were no errors, or sees a failed check and thinks the request failed. Why it happens: both sound like "did it go well?", but they measure different things. How to detect it: http_req_failed is about the transport (did the server respond with status < 400?); checks is about what you verified of the content (was the price 7500?). A request can have http_req_failed: 0% (it responded 200) but a failed check (the 200 carried the wrong price). How to fix it: read both: the error rate for availability, the checks for correctness (module 6).
Exercises
Exercise 1 — Find each instrument in the receipt. Using the summary above, say in which line and column you read: (a) the throughput (RPS), (b) the latency p95, (c) the error rate, (d) how many VUs ran.
See solution
- (a) Throughput (RPS): the
http_reqsline, in its rate:48.99/s. (The total, 2940, is the number of requests.) - (b) Latency p95: the
http_req_durationline,p(95)column:183ms. - (c) Error rate: the
http_req_failedline:0.00%(0 out of 2940). - (d) VUs: the
vus_maxline (or thescenariosheader):50.
Exercise 2 — Translate a summary into a verdict. You're given this excerpt of a k6 run: http_req_duration ... med=8ms p(95)=45ms, http_req_failed ... 3.20% 320 out of 10000, http_reqs ... 10000 500/s, agreed error threshold: 1%. (a) Does the test pass? (b) Which metric did you read first and why? (c) Does the 45 ms p95 change your verdict?
See solution
- (a) It doesn't pass. The error rate is 3.20%, above the agreed 1% threshold.
- (b) The error rate (
http_req_failed), because it validates everything else (lesson 5): if it fails, the verdict is already there, regardless of the latency. And it fails: 3.20% > 1%. - (c) No. With 3.20% error, the test already failed; the 45 ms p95 (which is itself reasonable) doesn't rescue it. The latency would only mean something if the error rate had passed. The p95 is useful later for diagnosing, not for rescuing the verdict.
Exercise 3 — iterations vs http_reqs in the receipt. In a summary, iterations: 2940 and http_reqs: 8820. (a) How many requests does each iteration make? (b) What kind of script produces that? (c) If the RPS (http_reqs /s) is 147/s, what's the iterations/s rate?
See solution
- (a)
8820 / 2940 = 3requests per iteration. - (b) A script whose
defaultfunction makes three HTTP requests —for example, aGET /rooms→POST /quote→POST /bookflow— per execution. (Module 6's.) - (c) The iterations rate is a third of the requests one:
147 / 3 = 49iterations/s. (Each iteration = a user completing the flow; 49 complete flows per second.)
Summary and next step
In this lesson you learned to read the k6 run summary as a detailed receipt: navigating its sections (TOTAL RESULTS, HTTP, EXECUTION, NETWORK) and knowing where each instrument lives —latency in http_req_duration (with its p(95)), error rate in http_req_failed, throughput in http_reqs and its /s rate, VUs in vus_max—. You saw that that block, although it goes as labeled content (k6 isn't installed), hides no magic: each line corresponds to something you already computed in Python —the p(95) is your statistics.quantiles, the RPS is your total / wall_time, the error rate is your errors / total—.
The skill you installed isn't computing (you already knew) but navigating: reading a k6 summary at a glance and drawing the verdict in the correct order (error rate first, then latency in percentiles, then throughput with its VU context). Before moving on you should be able to: locate the three instruments in a k6 summary; distinguish checks from http_req_failed; and explain why you don't need to run k6 to understand its output.
What comes next is the module's climax. In lesson 7 we return to the average trap, but now with full numerical force: over /quote_slow's distribution with a tail, you'll see measured how the average ends up above what 88% of people lived, and why real-world latency SLOs are written in percentiles and never in averages.
Resources
- k6 — End-of-test summary — the official reference for the summary block: its sections, which metric goes in each line, and how they're formatted. The source we verify this lesson's labeled content against.
- k6 — Built-in metrics (reference) — the catalog of
http_req_duration,http_req_failed,http_reqs,iterations,vus, and the rest, to look up what each line of the receipt measures. - k6 — Results and outputs — the overview of the ways to get results out of k6 (summary, JSON, CSV, streaming outputs); analyzing and exporting in depth is module 7.
- Module 2 of this guide — The k6 script and the virtual users — the anatomy of the script (
options,default,http.post,check,sleep) that produces this summary, reused here without re-explaining.