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.

FamilyQuestion it answersMetric in k6You see it executed in
LatencyHow fast does each request respond?http_req_duration (avg/med/p90/p95…)Python generator (lessons 2, 3, 7)
ThroughputHow many requests per second does it process?http_reqs (total + /s), iterationsPython generator (lesson 4)
Error rateWhat 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 /bookdoesn'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 endpointWhat it doesWhat we use it for
POST /quote_slowComputes 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_flakyComputes 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:

LessonWhat it installs
1. Introduction (this one)The three metric families; the two declared endpoints; the map
2. Latency and percentilesWhat latency is, its anatomy, client vs server, why not the average
3. p50/p90/p95/p99The four percentiles and how to compute them with statistics.quantiles
4. Throughput/RPS and VUsRPS, its relationship with VUs and think time (Little's law)
5. The error ratehttp_req_failed, and why low latency + errors = failure
6. Reading the k6 summaryThe k6 run block line by line (labeled content)
7. Average vs p95The climax: the tail, with measured numbers, and SLOs in percentiles
8. Mini-projectMeasure 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:

TopicWhere it livesWhy not here
What the metrics are and how they're computed/readThis module (3)
The anatomy of the script and the VUsModule 2 (already seen)Here we reuse them, we don't re-explain them
Load profiles: stages, ramps, spikesModule 4How the load varies over time; here the load is fixed and we measure
Thresholds: limits that make it pass/failModule 5A threshold is a limit on these metrics; the metric first
Checks and correlation (quote→book)Module 6Verifying correctness under load; here we measure performance
Analyzing trends and running in CIModule 7The "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 of http_req_duration.
  • (c) Error rate. The percentage of failed requests is http_req_failed in k6.
  • (d) Latency. The slowest request is the maximum of the latency; in k6, the max column of http_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 localhost and /quote responds 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_slow injects a long tail (declared, with a sleep) so that the high p95 is real and measurable.
  • (b) Because /quote is 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_flaky returns 500 on ~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