Module 1: Why Load And Performance Testing

1. Module introduction: two distinct questions

Overview

By the end of this lesson you'll have a clear grasp of the distinction that holds up the whole guide, and it's a sharper one than it's usually told. There are two questions you can ask a system, and they're different in nature. The first is does it work? —does it give the right answer when I ask it one concrete thing?—. The second is does it hold up? —does it keep giving that answer, and on time, when it isn't just me asking but five hundred people at once?—. The first question is about correctness; functional testing answers it (a unit test, an E2E test). The second is about load and performance; a load test answers it, and it's the subject of this guide. They're two independent axes: a system can be perfectly correct and still collapse with two hundred users; it can be blazing fast and still return the wrong price. You need to measure both, and with different tools.

You'll also meet Reservo, the API we work on across the eight modules: a minimal coworking room-booking server with three endpoints —list rooms, quote a price, and book—. Unlike the sibling Playwright guide, which tests a web page through the browser, here the subject under test is the API —the HTTP server that responds with JSON—, because load is measured against the backend, which is where traffic hurts. And —most important— you'll meet its anchor numbers: quoting three hours of the Focus room on the basic tier returns 7500 cents; on the pro tier, 6000. Those two numbers are the guide's "checksum": you'll see them appear again and again, and I actually ran them against the server before writing them here.

Connection to the module: this lesson is the map, not the territory. You don't write a complete load test yet —that comes in order: lesson 6 brings the API up, lesson 7 makes first executed contact, lesson 8 is the project—. What you install here is the idea that makes the other seven make sense: that "holds up" is a different question from "works," and that it's answered by measuring. Lesson 2 opens up the functional-vs-load distinction in depth. Lesson 3 names the three concrete questions of a load test (users, p95, breaking point). Lesson 4 introduces the five types (smoke, load, stress, spike, soak). Lesson 5 explains what k6 is and its own runtime. Lesson 6 builds and serves the Reservo API. Lesson 7 makes first contact —a k6 script as content and an executed Python generator—. And lesson 8, the project, puts you to measuring with your own hands.

A note on the boundary, because it sets the tone of the whole guide: here we don't test the correctness of the business logic. That price_cents("Focus", "basic", 3) equals 7500 is a fact you verify with a functional test (unit or E2E), and that lives in other guides of the Testing ecosystem. What we do here is take the logic's correctness for granted and ask something else: when that same correct logic receives hundreds of requests per second, how long does it take? how many can it handle before breaking? at what point does it fall over?. Keeping that boundary clear —correctness over there, load over here— is what lets you learn load testing without confusing it with the functional testing you probably already know.

The restaurant that serves a perfect dish... for one person

Think of it with a kitchen. A chef prepares a risotto and tastes it: the rice's texture, the salt, the temperature. It's perfect. That tasting answers one question —is the dish well made?— and answers it with a resounding yes. That's functional testing: you fix an input (this recipe, these ingredients) and verify the output is correct (a risotto cooked just right). You can taste a thousand different dishes and each tasting tells you whether that dish came out well.

But now open the restaurant on a Friday at nine at night. Eighty diners come in over twenty minutes and all order risotto. The dish is still perfect —the recipe didn't change— but questions appear that the tasting never asked: are there enough burners for eighty risottos at once, or does a queue form and the last plates come out cold after a forty-minute wait? At what number of diners does the kitchen stop keeping up —seventy, eighty, a hundred—? If a tour bus suddenly drops in and fifty orders land in two minutes, does the kitchen absorb the spike or collapse? Those are questions of load and performance. They don't ask whether the dish is good; they ask whether the kitchen holds up when it's seriously in demand, and how fast it serves under that pressure.

Notice that both kinds of test are necessary and neither replaces the other. A restaurant with a perfect risotto that takes forty minutes to serve on a Friday has a real problem —a performance one— that no tasting would have caught, because the tasting is always done with an empty kitchen. And the other way around: a lightning-fast kitchen that serves a salty risotto has a correctness problem that no load test would catch, because a load test measures times and volume, not taste. The chef needs to taste (functional) and simulate Friday night (load). This whole guide is about the second: how to simulate your API's Friday night, measure what happens, and set a threshold that says "if 95% of the plates don't come out in under half a second, this isn't ready to open."

It's worth spelling out in full, because it's the idea that runs through all eight modules:

Functional testing asks "does it give the right answer?" (correctness). Load and performance testing asks "does it hold up when many users hit it at once, and how fast does it respond under pressure?" (performance). They're two independent axes: you need both, and they're measured with different tools. This guide is about the second.

The guide's study API: Reservo

The whole guide works on the same made-up API, for the same reason a cook practices a Friday service always in the same kitchen: if every lesson debuted a new system, you'd spend half your attention understanding the context instead of learning to measure load. With a single API, by module 3 you'll know Reservo by heart and can concentrate on what each lesson adds —metrics, profiles, thresholds— without asking yourself again "wait, what did this endpoint return?".

What Reservo is and why it's the perfect target

Reservo is the room-booking API of a coworking space. A coworking has rooms of different sizes —a call booth (Focus), a studio (Studio), a boardroom (Boardroom)— and members book them by the hour. The Reservo API does three things: list the rooms and their rate, quote (given room, tier, and hours, say how much it costs), and book (confirm). Nothing more. It's deliberately small.

What makes Reservo the perfect target for learning load testing is exactly what it doesn't have. It has no database, no login, no heavy framework: it's a single Python file that uses http.server from the standard library —the smallest HTTP server there is, and one that ships with Python—. Why does that simplicity matter? Because it lets us concentrate on the load-testing technique —launching concurrent traffic, measuring latency, computing percentiles, setting thresholds— without drowning in infrastructure setup. And at the same time it has everything a load test needs to practice on: real HTTP endpoints, a GET and two POSTs with JSON bodies, a response with a number we can verify. It's a load target in miniature, but complete.

Some honesty up front, in the spirit of the guide: Reservo is a lab API. It runs on localhost, so the network isn't a factor and the latencies we measure will be very low (milliseconds or fractions). A real API lives behind the network, with a database that is almost always the true bottleneck, and its latencies under load would be higher. Reservo keeps the heart —HTTP endpoints that respond under concurrent traffic— and throws out the rest, precisely because that heart is what you have to learn to measure first. The technique you practice here is identical to the one you'd apply against a production API; only the numbers change.

The canonical API: what it looks like and what it contains

This is the API you'll build in lesson 6 and measure for the rest of the guide. Here we present its contract —what endpoints it has and what each one returns— so you have it as a reference from minute one. The identifiers, fields, and values are in English (room, tier, hours, price_cents) because that's the real tech market; the prose that explains them is in English too (in this translated version).

Method and pathRequest bodyResponseWhat for
GET /rooms{"rooms": [{"room": "Focus", "hourly_cents": 2500}, ...]}List the rooms and their hourly rate (in cents).
POST /quote{"room": "Focus", "tier": "basic", "hours": 3}{"price_cents": 7500}Quote: given room, tier, and hours, return the price in cents.
POST /book{"room": "Focus", "tier": "basic", "hours": 3}{"booking_id": "bk_Focus_basic_3", "price_cents": 7500, "confirmed": true}Book: confirm and return a booking identifier.

The hourly rates, in integer cents (never a float for money), are: Focus 2500, Studio 4000, Boardroom 8000. The price of a quote is rate × hours, and if the tier is pro an integer 20% discount is applied —price * 80 // 100, with integer division, no decimals—. Don't memorize the detail; we build it calmly in lesson 6. For now keep the shape: a GET to list, a POST /quote that returns a price_cents, and a POST /book that confirms. Those are the three endpoints the Python generator (executed) and the k6 script (content) will hit with load.

The anchor numbers

Here is the numeric heart of the guide. These exact results are what the API returns, and what we'll verify under load module after module. They're the "checksum": if the API reproduces them, the logic is correct and we can focus on its performance; if not, there's a correctness bug (which is another guide's topic). I actually ran them against the Reservo server running on localhost, with curl, before writing them here —they're not made up, they're measured—:

EndpointInputCalculation (cents)Real response
POST /quoteFocus / basic / 3h2500 × 3 = 7500{"price_cents": 7500}
POST /quoteFocus / pro / 3h7500 × 80 // 100 = 6000{"price_cents": 6000}
POST /quoteStudio / basic / 1h4000 × 1 = 4000{"price_cents": 4000}
POST /bookFocus / basic / 3h{"booking_id": "bk_Focus_basic_3", "price_cents": 7500, "confirmed": true}

The first two —7500 and 6000— are the anchors you'll see most. In a load test we'll use them in two ways: as the traffic we launch (thousands of Focus/basic/3h quotes) and as the correctness check under load (verifying that, even with 500 concurrent requests, the API still returns 7500 and not an error). That the response stays correct under pressure is itself something load puts to the test.

The map: the eight lessons and the guide

This module goes from understanding why load is a different question to knowing the tools and the types to bringing up the API to measuring for the first time. Each lesson leaves a piece:

LessonWhat it installs
1. Introduction (this one)The functional-vs-load distinction, the Reservo API, the anchor numbers, the map
2. Functional vs loadThe two questions in depth: correctness vs performance under pressure
3. The questions a load test answersConcurrent users, p95, breaking point; why the average lies
4. The test typesSmoke, load, stress, spike, soak: what each one asks
5. What k6 isThe tool, its own runtime (not Node), its place
6. The API and serving itBuilding the Reservo server and seeing it respond
7. First contactA k6 script (content) and a Python generator (executed)
8. Mini-projectYour first real load measurement, with your own hands

And the whole guide, beyond this module, follows this arc: module 1 gives you the why and first contact; 2, the anatomy of the k6 script and VUs (virtual users); 3, the metrics in depth (latency, percentiles, throughput, errors); 4, the load profiles (stages, ramps, executors); 5, the thresholds (the limits that make the test pass or fail, a quality gate); 6, checks, groups, and realistic scenarios (verifying correctness under load, correlating a quote→book flow); 7, analyzing results and running in CI; and 8, a capstone project with a complete Reservo load test.

The environment rule: what runs and what is content

This guide has an honesty rule worth understanding right away, because it explains why you'll sometimes see "real" output and sometimes "here's how it would look." k6 is a separate binary (written in Go) that isn't installed in the environment where this guide was prepared. So everything k6 —its .js scripts and its output summary (with http_req_duration, p95, checks)— is presented as clearly labeled content: it's correct and faithful to k6's official documentation, but it was not run here, and we'll never present it to you as if it had been.

By contrast, everything Python is run and cited: the local Reservo API, and a mini load generator that launches concurrent requests and measures real latencies with concurrent.futures and urllib. That way, even though the k6 runner is content, you see the load concepts —latency, percentiles, throughput, breaking point— with actually measured numbers. The Python generator is the k6 script's "executable sibling": it does conceptually the same thing (launch traffic and measure), at a smaller scale, so you can touch the numbers with your hands. When we get to CI in module 7, the GitHub Actions file will also be content. And at no point does this guide run git or gh.

The boundary: what's taught here and what's in the sibling guides

This guide is the load tip of the testing pyramid, and it has a clear rule about what's its job. Its job is performance and load testing with k6. Everything else is taught in the sibling guides of the Testing ecosystem, and is assumed or linked here:

TopicWhere it lives
What a test is, assert, pytest, TDDtesting-fundamentals-and-tdd-guide (assumed)
Functional E2E through the browser (UI correctness)e2e-testing-with-playwright-guide (sibling guide)
Performance and load (does it hold up? how fast under pressure?)This guide
Optimizing the app/DB (indexes, cache) after finding the bottleneckOut of scope (mentioned as the "after")

The mechanical rule to remember it: if the question is "does it give the right answer?" —whether an isolated function or a flow through the browser—, it's functional testing (another guide). If it's "how many users does it hold and how fast does it respond under pressure?", it's this guide. The Playwright guide and this one are close cousins —both test the system "for real," not isolated functions— but they ask different things: Playwright drives the browser to verify that the user sees the right thing (correctness); k6 and the Python generator hit the API with volume to measure whether it holds up (load). Same Reservo, two questions.

Common mistakes

Believing a green functional suite means the system "is ready." What happens: all the unit and E2E tests pass, so the system is declared production-ready —and on launch day, with real traffic, the API responds in eight seconds or returns 500 errors—. Why it happens: correct gets confused with ready. A functional suite tests correctness with an empty kitchen; it never simulates Friday night. How to detect it: if you never measured how many concurrent requests your system holds or what its p95 under load is, you don't know if it's ready, however green your functional suite is. How to fix it: add a load test to your definition of "ready." That is, literally, what this guide exists for.

Thinking that "faster on my machine" equals "fast under load." What happens: someone measures a single request, sees it takes 3 milliseconds, and concludes the API is fast. Then, under 200 concurrent users, that same API takes 800 milliseconds because the requests queue up. Why it happens: an isolated request doesn't compete for resources; hundreds of concurrent ones do. Latency under load is a different property from latency at rest. How to detect it: if your only measurement was one curl, you measured the rest state, not load. How to fix it: measure with concurrency —it's exactly what the Python generator in lesson 7 does, and you'll see it: the same API goes from a p95 of 0.35 ms with no contention to tens of milliseconds with 50 clients at once—.

Confusing this guide with the Playwright one because "both test the whole app." What happens: someone tries to use k6 to verify that a button on the page works, or Playwright to measure how many users the backend holds. Why it happens: both tools exercise the system "for real," and it's easy to mix them up. How to detect it: if your question is "does the user see the right result on the screen?", you're in Playwright's territory (UI correctness); if it's "how many requests per second does the API hold and at what p95?", you're in k6's territory (load). How to fix it: use the boundary rule —correctness through the browser → Playwright; load against the API → this guide— and link them, don't mix them.

Exercises

Exercise 1 — Classify: functional or load? For each question about Reservo, decide whether it's answered by a functional test (correctness) or a load test (performance under pressure), and explain why in one sentence. (a) "Does POST /quote of Focus/basic/3h return 7500?" (b) "How many quotes per second does the API hold before latency spikes?" (c) "Is the 20% pro discount applied correctly for 1, 2, and 3 hours?" (d) "With 300 users quoting at once, what's the p95 latency?"

See solution
  • (a) Functional (correctness). It fixes an exact input and verifies an exact output; it's tested with a single request, no volume. That's the job of a functional test (unit or E2E), not a load test.
  • (b) Load (performance). It asks about capacity —how many requests per second before degrading—, which is only answered by launching increasing traffic and measuring. It's a load test (a stress type, as you'll see in lesson 4).
  • (c) Functional (correctness). These are three verifications of the same calculation logic, varying only the hours. They're tested with three isolated requests, verifying the number. Pure correctness; not a drop of load.
  • (d) Load (performance). It fixes a concurrency level (300 users) and measures a latency metric (p95). It's exactly the kind of question a load test answers, and p95 is the central metric (lesson 3).

The lesson: if the question fixes one input and verifies one output, it's functional; if it fixes a traffic level and measures times or volume, it's load. (a) and (c) are functional; (b) and (d) are load.

Exercise 2 — Read the API contract. Without running anything yet, look at the canonical API table above and answer: (a) What method and path would you use to quote, and what fields go in the request body? (b) Based on the rates and the pro discount, what does POST /quote return (price_cents) for {"room": "Studio", "tier": "pro", "hours": 2}? (c) What three fields does the POST /book response carry?

See solution
  • (a) POST /quote, with a JSON body {"room": ..., "tier": ..., "hours": ...} —the three fields: the room, the tier, and the hours—. Note that it's a POST (it carries a body), not a GET.
  • (b) Studio costs 4000 cents/hour. Without a discount it'd be 4000 × 2 = 8000. The pro tier applies an integer 20% discount: 8000 × 80 // 100 = 6400. The response is {"price_cents": 6400}. (All in integer cents; never a float.)
  • (c) booking_id (the booking identifier, e.g. "bk_Focus_basic_3"), price_cents (the confirmed price), and confirmed (true).

Exercise 3 — Place each need in its guide. For each situation, decide whether you solve it with this guide (load with k6), the Playwright one (E2E), or the fundamentals one, and say which in one sentence. (a) "I want to verify that, when clicking Quote on the page, the user sees $75.00." (b) "I want to know if my API holds 500 concurrent bookings without falling over." (c) "I want to understand what assert is and write my first test." (d) "I want to measure how much the p95 latency of /quote rises when I go from 50 to 200 users."

See solution
  • (a) Playwright (E2E). "The user, on the page, sees the right result" is correctness through the browser: the territory of e2e-testing-with-playwright-guide.
  • (b) This guide (load with k6). "Does it hold 500 concurrent without falling over?" is capacity under load: exactly what a load test measures (a stress test, lesson 4).
  • (c) Fundamentals: testing-fundamentals-and-tdd-guide. What a test is, assert, and pytest are the foundation of the pyramid; both tip guides (Playwright and this one) assume them.
  • (d) This guide (load with k6). Measuring how the p95 changes as concurrency rises is the heart of performance testing —in fact, it's what the Python generator in lesson 7 does, and you'll see it with real numbers—.

The mechanical rule: "does it give the right thing?" through the browser → Playwright; "what is a test?" → fundamentals; "does it hold up and how fast under load?" → this guide.

Summary and next step

In this lesson you installed the idea that holds up the eight modules: there are two distinct questions you can ask a system. Functional testing asks does it give the right answer? (correctness); load and performance testing asks does it hold up when many users hit it at once, and how fast does it respond under pressure? (performance). They're independent axes —correct doesn't imply ready, fast doesn't imply correct— and you need to measure both. This guide is about the second, like the chef who doesn't just taste the risotto but also simulates Friday night.

You met Reservo, the canonical API we work on all guide long: a minimal server with GET /rooms, POST /quote {room,tier,hours}{price_cents}, and POST /book{booking_id,confirmed}, with prices in integer cents and an integer pro discount. And above all the anchor numbers —Focus/basic/3h → 7500, Focus/pro/3h → 6000—, actually measured against the server on localhost. The environment rule is also clear: Python is run and cited; k6 is content, labeled, correct but not executed.

Before moving on you should be able to: state in your own words the difference between a functional question and a load one; explain why a green functional suite doesn't guarantee the system holds production; name Reservo's three endpoints and its two anchor numbers (7500 and 6000); and recall the boundary with the Playwright guide (correctness through the browser over there, load against the API over here).

What comes next is going deeper into that central distinction. In lesson 2 we open up the functional vs load contrast in full: how a functional test fixes the input and verifies the output, how a load test fixes the traffic and measures latency/throughput/errors, and why one and the other can deliver opposite verdicts on the same system.

Resources