Module 3: Metrics Latency Throughput Errors
1. Module introduction: the test's dashboard
Overview
In the first two modules you learned two big things. In module 1, that "holds up" is a different question from "works," and that it's answered by measuring. In module 2, how a k6 script is written and how a handful of VUs —virtual users, threads that hit the API in parallel— generate traffic against Reservo. You know how to generate load. What you still don't know is how to read what that load produces, and without that a load test is worth nothing. A k6 run ends and spits out a screen of numbers; if you don't know what each one means, you have noise, not information. This module teaches you to read the dashboard.
A load test's dashboard has exactly three instruments, and this module installs them one by one. The first is latency: how long each request takes. It seems simple, but it hides the most common trap of all performance testing —looking at the average—, and a good part of the module is teaching you to look instead at the percentiles (p50, p90, p95, p99). The second is throughput: how many requests per second the system processes (RPS), and how it relates to how many VUs you launch. The third is the error rate: what percentage of requests failed, the metric that can throw a very low latency in the trash. Latency, throughput, errors. With those three numbers, read well, you know almost everything a load test has to tell you.
Connection to the module: this lesson is the map. It doesn't yet compute any percentile or bring up the generator —that starts in lesson 2 and doesn't stop until lesson 8's project—. What it installs here is the structure: the three metric families, in which lesson each one lives, and two infrastructure decisions that make the module possible. Because Reservo runs on localhost and is very fast, if we measured its normal /quote the p95 would come out in fractions of a millisecond and you wouldn't see the average-vs-tail lesson with force. So this module declares —says so openly— two extra endpoints on top of the canonical API: one slow with a long tail (to see a truly high p95) and one flaky that fails now and then (to see a real error rate). The canonical API doesn't change; only two lab endpoints are added, and this lesson explains which ones and why.
The usual honesty, because it sets the tone: the numbers you'll see measured are measured. The Python load generator hits Reservo for real, times each request for real, and statistics.quantiles computes the percentiles for real —I ran them before writing this guide and you'll see the real terminal output—. The only thing presented as labeled content is the k6 run summary, because k6 is a binary that isn't installed here; that block is faithful to k6's official format, but we'll never present it to you as if we'd run it.
A car's dashboard: three instruments, not one
Think of a car's dashboard when you're on the highway. If you could only look at one instrument, which would you choose? Almost everyone says "the speedometer." But the speedometer only tells you how fast you're going; it doesn't tell you whether the engine is about to blow (that's the temperature gauge), or how much fuel you have left (the fuel gauge), or whether something broke (the warning lights). A driver who only looks at the speedometer gets there very fast... until they run out of gas in the middle of nowhere, or blow the engine. Speed alone lies by omission: it's a real number, but incomplete.
A load test has the same problem, and that's why it also has three instruments. Latency is the speedometer: it tells you how fast the system responds. Throughput is the flow odometer: how many requests per second you're processing. And the error rate is the warning lights: how many requests broke along the way. Looking at only one of the three fools you just like the driver. An API with very low latency but 30% errors is a car flying along... with the engine on fire. An API with very high throughput but an eight-second p95 is a car moving a lot of cargo... but every delivery arrives extremely late. You need the three numbers, and you need to read them together.
Notice that the most treacherous instrument is latency, and not because it's hard to measure, but because it's easy to summarize badly. If you ask someone "how long does your API take?", they almost always answer with an average: "about 30 milliseconds." And the average, for latencies, is like telling a passenger "on average we're comfortable" when the car slams on the brakes brutally every two minutes: technically true, humanly false. The middle half of this module is dismantling that trap and teaching you to look at percentiles, which do tell the truth about what each user experiences.
A load test has three metric families: latency (how fast?), throughput (how much volume?), and error rate (how much broke?). They're read together: any of the three, alone, deceives. And latency, in particular, is never summarized with the average —it's looked at in percentiles—.
The three metric families, and their name in k6
Each of the three families has a concrete name in the k6 summary. I present them here in a table so you have the complete map from minute one; each lesson develops one row.
| Family | Question it answers | Metric in k6 | You see it executed in |
|---|---|---|---|
| Latency | How fast does each request respond? | http_req_duration (avg/med/p90/p95…) | Python generator (lessons 2, 3, 7) |
| Throughput | How many requests per second does it process? | http_reqs (total + /s), iterations | Python generator (lesson 4) |
| Error rate | What percentage of requests failed? | http_req_failed (% failed) | Python generator (lesson 5) |
All three are computed over the same thing: a list of requests, each with its measured latency and its result (success or failure). The Python generator you already know from module 1 launches N concurrent requests, saves each one's latency and whether it succeeded, and from that list comes everything: the latency percentiles, the RPS (dividing the total by the wall-clock time), and the error rate (counting the failed ones). k6 does exactly the same, industrialized and at a larger scale. That you can compute all three metrics with twenty lines of Python is the best proof that they're not magic: they're basic statistics over a list of numbers.
This module's two declarations: a slow endpoint and a flaky one
Here's the infrastructure decision that makes the module possible, and I declare it in full because the guide's rule is to hide nothing. The canonical Reservo API —GET /rooms, POST /quote, POST /book— doesn't change: it still returns 7500 for Focus/basic/3h and 6000 for Focus/pro/3h, identical to how you knew it in module 1. But Reservo runs on localhost and is so fast that its /quote responds in fractions of a millisecond. With those numbers, the module's most important lesson —"the average hides the tail"— would come out blurred, because there's almost no tail to hide.
So this module adds two lab endpoints to the same server, and uses them only to be able to measure phenomena that on the real /quote would be invisible:
| Declared endpoint | What it does | What we use it for |
|---|---|---|
POST /quote_slow | Computes the same price as /quote, but simulates a slow dependency: 90% of requests respond fast and ~10% suffer a big spike (a long tail). | See a truly high p95/p99 and the difference between average and percentiles (lessons 2, 3, 7). |
POST /quote_flaky | Computes the same price and responds fast, but ~10% of the time returns a 500 error instead of the price. | See a real error rate and why low latency + errors = failure (lesson 5). |
Both are honest about what they are: simulations. A production API would have a long tail because its database sometimes takes long, and errors because sometimes a service it depends on goes down. Reservo has neither a database nor dependencies, so we fabricate them in the simplest way possible (a sleep with a tail, a random 500) so you can practice measuring and interpreting those phenomena with real data. The technique you learn —computing the p95 of a distribution with a tail, reading an error rate— is identical against a real API; only the origin of the slowness and the failures changes.
The map of the eight lessons
This module goes from understanding what each metric is to computing it for real to reading it in the k6 summary to putting it all together on Reservo. Each lesson leaves a piece:
| Lesson | What it installs |
|---|---|
| 1. Introduction (this one) | The three metric families; the two declared endpoints; the map |
| 2. Latency and percentiles | What latency is, its anatomy, client vs server, why not the average |
| 3. p50/p90/p95/p99 | The four percentiles and how to compute them with statistics.quantiles |
| 4. Throughput/RPS and VUs | RPS, its relationship with VUs and think time (Little's law) |
| 5. The error rate | http_req_failed, and why low latency + errors = failure |
| 6. Reading the k6 summary | The k6 run block line by line (labeled content) |
| 7. Average vs p95 | The climax: the tail, with measured numbers, and SLOs in percentiles |
| 8. Mini-project | Measure Reservo's metrics with your own hands and interpret them |
And in the whole guide, this module is the hinge. Modules 1 and 2 gave you the why and the tool; modules 4 onward teach you to use the metrics to decide. In module 4 you model load profiles (stages, ramps) and observe how the p95 changes as you vary the shape of the load —but to read that change, you first have to know what the p95 is, and that you install here—. In module 5 you set thresholds: limits on these same metrics (p(95)<500, rate<0.01) that make the test pass or fail —but a threshold is a limit on a metric, so it makes no sense until the metric is yours—. In 6 you verify correctness under load with check(); in 7 you analyze and run in CI. Everything that comes relies on your knowing how to read latency, throughput, and errors. This is the module that gives you the eyes.
The boundary: what's this module's and what's the next ones'
So you don't mix them up, here's the exact line between this module and what's coming, because it's easy to jump ahead:
| Topic | Where it lives | Why not here |
|---|---|---|
| What the metrics are and how they're computed/read | This module (3) | — |
| The anatomy of the script and the VUs | Module 2 (already seen) | Here we reuse them, we don't re-explain them |
| Load profiles: stages, ramps, spikes | Module 4 | How the load varies over time; here the load is fixed and we measure |
| Thresholds: limits that make it pass/fail | Module 5 | A threshold is a limit on these metrics; the metric first |
| Checks and correlation (quote→book) | Module 6 | Verifying correctness under load; here we measure performance |
| Analyzing trends and running in CI | Module 7 | The "after" of having the metrics |
The mechanical rule: if the question is "what does this number mean and how do I get it?" —a p95, an RPS, an error rate—, it's this module. If it's "how do I make the load go up and down?", it's module 4. If it's "how do I make the test fail when the p95 exceeds 500 ms?", it's module 5. Here we stay on understanding and computing; using the metrics to decide comes later.
Common mistakes
Reporting a load test with a single number. What happens: someone runs k6 and summarizes "the API runs at 30 ms." 30 ms of what? The average? The p95? With how many VUs? How many errors were there? Why it happens: latency gets treated as if it were a single datum, when it's a distribution with many faces. How to detect it: if your report fits in one number, you're missing information. How to fix it: always report the three families —latency (with at least p50 and p95), throughput (RPS), and error rate— and the context (how many VUs). A single number isn't a load report.
Ignoring the error rate because "the latency looks fine." What happens: the test shows a p95 of 10 ms, gorgeous, and the API is declared perfectly healthy —without noticing that 10% of the requests returned 500—. Why it happens: latency is the flashiest instrument and steals the attention; the error rate is read out of the corner of the eye. How to detect it: if you never looked at http_req_failed, your "fast" verdict isn't worth it. A request that fails fast is still a request that failed. How to fix it: always read the error rate before celebrating the latency. You'll see it harshly in lesson 5, with the flaky endpoint: 10 ms p95 and 10% error is a failure, not a success.
Believing the slow and flaky endpoints are "cheating" or "made-up data." What happens: someone sees /quote_slow and thinks we're making up the numbers. Why it happens: they confuse simulating a phenomenon with inventing a measurement. How to detect it: the right question isn't "is the slowness real?" but "is the measurement of that slowness real?". How to fix it: understand the distinction. The slowness is simulated with a sleep (declared); but the p95 the generator computes over those latencies is a real measurement of a real distribution. In production the slowness would come from a database instead of a sleep, and you'd measure it exactly the same. Simulating the cause to be able to practice the measurement is legitimate and is declared; fabricating the result without measuring wouldn't be, and we don't do it.
Exercises
Exercise 1 — Which metric family answers each question? For each question about Reservo under load, say whether it's answered by latency, throughput, or error rate, and name the corresponding k6 metric. (a) "How many quotes per second does the API process?" (b) "In how much time did 95% of the users receive their price?" (c) "What percentage of bookings returned a 500 error?" (d) "How long did the slowest request take?"
See solution
- (a) Throughput. Requests per second is exactly RPS; in k6,
http_reqs(reported as total and as a rate/s). - (b) Latency. "95% received their price in X time or less" is the p95 of the latency; in k6, the
p(95)column ofhttp_req_duration. - (c) Error rate. The percentage of failed requests is
http_req_failedin k6. - (d) Latency. The slowest request is the maximum of the latency; in k6, the
maxcolumn ofhttp_req_duration.
The rule: "per second" → throughput; "how long it took" (a percentile, the max, the median) → latency; "what percentage failed" → error rate.
Exercise 2 — Why are two extra endpoints declared? In one or two sentences each: (a) Why does this module add /quote_slow instead of measuring the normal /quote for the percentiles lesson? (b) Why does it add /quote_flaky instead of waiting for /quote to fail on its own?
See solution
- (a) Because Reservo runs on
localhostand/quoteresponds in fractions of a millisecond, almost without a tail. With such a flat distribution, the difference between average and p95 would be minimal and the lesson "the average hides the tail" wouldn't be seen with force./quote_slowinjects a long tail (declared, with asleep) so that the high p95 is real and measurable. - (b) Because
/quoteis correct and never fails under this load, so its error rate would always be 0% and there'd be nothing to learn to read./quote_flakyreturns500on ~10% of the requests (declared) so there's a real error rate to measure and interpret.
In both cases the cause of the phenomenon is simulated, but the measurement of the phenomenon is real. That's what's being practiced.
Exercise 3 — The car with a single instrument. For each situation, say what's wrong with the verdict and which metric is missing. (a) "The p95 is 8 ms, the API flies, ready for production." (The error rate wasn't checked; it's 12%.) (b) "We process 5,000 RPS, excellent." (The latency wasn't checked; the p95 is 4 seconds.) (c) "The average latency is 30 ms, perfect." (The p95 wasn't checked; it's 190 ms.)
See solution
- (a) The verdict looks only at the latency and ignores the error rate. A p95 of 8 ms with 12% errors means 1 in every 8 users received a fast failure: quick, but useless. It's missing a look at
http_req_failed. - (b) The verdict looks only at the throughput and ignores the latency. Moving 5,000 RPS with a p95 of 4 seconds means processing a lot of volume while each user waits an eternity. It's missing a look at
http_req_duration(the percentiles). - (c) The verdict looks at the average instead of the percentile. An average of 30 ms with a p95 of 190 ms betrays a tail: the low average hides that 1 in every 20 users waited six times longer. It's missing a look at the p95 (you'll see it measured in lesson 7).
The lesson of the three: no instrument, alone, gives the verdict. Latency, throughput, and errors are read together, and latency always in percentiles.
Summary and next step
In this lesson you built the module's map. A load test is read with three metric families: latency (http_req_duration in k6: how fast?), throughput (http_reqs: how much volume per second?), and the error rate (http_req_failed: how much broke?). They're the test's dashboard, and like a car's dashboard, they deceive if you look at only one instrument: they're read together, and latency never with the average.
You met the two declarations that make the module possible: on top of the same canonical Reservo API (which still returns 7500 and 6000, intact), we add /quote_slow —slow, with a long tail, to see a truly high p95— and /quote_flaky —which fails ~10%, to see a real error rate—. The cause of the slowness and the failures is simulated and declared; the measurement of both is real and you'll see it come out in the terminal. And the boundary is clear: here you learn what the metrics are and how they're computed; using them to vary the load (M4), to make the test pass or fail (M5), or to analyze and run in CI (M7) comes later.
Before moving on you should be able to: name the three metric families and their k6 metric; explain why they're read together; and say in your own words why this module declares a slow endpoint and a flaky one. What comes next is opening the first instrument. In lesson 2 we get into latency: what exactly http_req_duration measures, how it decomposes (sending + waiting + receiving), the difference between the latency the client sees and the one the server processes —measured for real— and the first concrete proof of why the average lies.
Resources
- k6 — Built-in metrics (reference) — the official catalog of the metrics k6 produces:
http_req_duration,http_reqs,http_req_failed, and the rest. It's the source we verify this module's labeled k6 content against. - Google SRE Book — Monitoring Distributed Systems — the frame for why performance is measured in several signals (latency, traffic, errors, saturation) and in percentiles, not averages. The conceptual background of the whole module.
statistics— Python documentation — the standard-library module withquantilesandfmean, with which we compute the percentiles and the average for real starting in lesson 3.- Module 2 of this guide — The k6 script and the virtual users — the anatomy of the script and the VU model we reuse here. If you're not clear on what a VU is or how load is launched, review it before continuing.