Module 8: Project Load Test Reservo
1. Module introduction: assembling the complete test
Overview
You reached the end. Over seven modules you learned to produce a load test piece by piece: why it exists (module 1), how a k6 script is written and what virtual users are (module 2), how p95 latency, throughput, and error rate are measured (module 3), how the load is shaped with profiles and stages (module 4), how a threshold turns a metric into a pass/fail verdict (module 5), how a check() verifies correctness under load (module 6), and how the result is analyzed and automated in CI (module 7). Each module left a piece on the table. This module is where all the pieces are assembled into a single complete test of the Reservo API, and you deliver it.
There's no new topic here. It's an integration capstone: the work of bringing together what you already know into a single end-to-end artifact, the one you'd write the first day someone tells you "put a load test on this API." You'll build four things that fit together —the k6 script (content), the canonical API as the target, the executable run in Python with its metrics and its gate, and the CI (content)— and close them with a rubric of what makes a load test good.
Connection to the module: this lesson is the capstone's map. It doesn't explain a new concept; it explains how the seven previous modules come together, what each contributes, and what you'll deliver. It fixes —for the last time— the environment rule (what's run and what goes as content), reuses the /quote_cpu endpoint module 5 declared to be able to provoke the degradation, and draws the boundary with what stays outside the guide. By the end of the module you'll have done, with your own hands, a complete load test: the whole guide's arc in a single deliverable.
The orchestra that plays the complete symphony
Think of an orchestra. For months, each section rehearsed separately: the violins tuned their part, the brass theirs, the percussion marked its beat, the conductor studied the score. Each separate rehearsal was necessary —without it, no one would master their instrument— but none was the symphony. The symphony is concert night: all the sections playing at once, in the correct order, each coming in when it's their turn, forming a single piece the audience hears complete.
The seven previous modules were the sectional rehearsals. You learned the script (the violins), the metrics (the brass), the profiles (the percussion), the thresholds (the conductor marking "no further than here"). You practiced each one in isolation, against a piece of the problem. This module is the concert: the quote→book scenario (M2, M6) playing under a smoke→load→stress profile (M4), measured with p95/RPS/error (M3), judged by thresholds tied to an SLO (M5), analyzed and exported (M7), and all put in a pipeline (M7). No section is new; what's new is that they sound together, and that at the end you have a whole load test —not seven isolated exercises—. Mastering each instrument was the modules' work; playing the symphony is this one's.
The four pieces you'll assemble
A complete, deliverable load test has four pieces. Each comes from previous modules; the capstone joins them.
- The k6 script (content). The artifact a team with k6 installed would run with
k6 run. It joins thedefaultfunction with the quote→book scenario and itscheck()s (M2, M6), thesleep()with jitter (M6), andexport const optionswithstagessmoke→load→stress (M4) andthresholdstied to the SLO (M5). Since k6 isn't installed, it goes labeled as content, faithful to the official documentation. - The canonical Reservo API (the target). The local server you already know since module 1:
GET /rooms,POST /quote→{price_cents},POST /book→{booking_id, confirmed}, with the anchor numbers 7500 and 6000. It runs onlocalhoston port 0. It's what the test hammers. - The executable run in Python. The generator that runs the quote→book scenario in stages (smoke→load→stress) against the API, measures real p50/p95/p99, RPS, and error rate, evaluates the thresholds over the whole run —like k6—, and exits with an exit code (0 = PASS, 1 = FAIL), exporting its metrics to a
results.json. This one is actually run and its output is cited. - The CI (content). The
.github/workflows/load.ymlwhere the threshold acts as a gate that blocks the deploy if the performance doesn't meet it. It goes as content, faithful to GitHub Actions and k6's official integration.
Lessons 2 to 7 build these pieces one by one; lesson 8 joins them into the deliverable and puts a rubric on them.
The endpoint we reuse: /quote_cpu (from M5)
A load test that always passes teaches nothing. To see the gate go red you need a target that, under load, degrades its p95 over the limit. The canonical Reservo API is very fast on localhost (POST /quote responds in under a millisecond), so on its own it would never cross a reasonable SLO. We need a target whose response time depends on the load.
As module 5 declared and this capstone reuses, that target is /quote_cpu: it does exactly the same as /quote (receives {room, tier, hours}, returns {price_cents} with the same anchor numbers 7500 and 6000) but before responding it does CPU work —a loop that burns cycles, modeling a "realistic" pricing engine that actually computes—. The key is in Python's GIL: the CPU work does not run in parallel between threads, it's serialized. With few clients at a time (smoke, load) there's little queue and the p95 stays low; with many (stress), the CPU-work queue grows and the p95 spikes. It's the signature of an endpoint whose bottleneck is the computation: cheap when no one uses it, expensive when everyone uses it at once.
With this we have the two faces the capstone needs: the healthy load against /quote (the fast build, which passes the SLO) and the degradation against /quote_cpu (the heavy pricing engine, whose p95 crosses the limit under the stress). The same test, the same threshold, two targets: one green, one red.
The environment rule (one last time, because it matters)
This capstone has two protagonists in different categories, and confusing them would ruin what the whole guide taught:
- What's actually run and cited is Python. The Reservo API running on
localhost; the generator that runs the scenario in stages and measures real p50/p95/p99, RPS, and error rate; the threshold evaluation that exits with an exit code; theresults.jsonthat's exported. When you see a block with apython3.14 ...command and anexit code, that actually happened in this environment with Python 3.14.0. - k6 and CI go as labeled content. k6 isn't installed (it's a Go binary with its own JavaScript runtime; node doesn't run it). The complete
.jsscript, its summary with the thresholds section (✓/✗), and the.github/workflows/load.ymlare content, faithful to k6's and GitHub Actions' official documentation —never a fabricated output presented as executed—. When you see a k6 block or a CI YAML, it will be labeled as content.gitandghare never run here.
This separation is what makes the capstone's learning solid. You see the real mechanism —run the scenario in stages, measure the p95, evaluate the threshold, fail with an exit code, export to a file— with your real metrics in Python; and you see the industrial form of that same mechanism in the k6 script and in the CI YAML. They're the same test at two scales.
This module's boundaries
- Each individual topic (the script, the metrics, the profiles, the thresholds, the checks, the analysis, the CI) is its module's. Here they're not re-explained; they're integrated. If you're unsure of a piece, the module that taught it is a link away.
- Testing correctness through the browser (that a user who picks Focus/basic/3h sees
$75.00on the screen) is E2E, and lives in the sibling guidee2e-testing-with-playwright-guide. Here we test the API's load, not the UI's correctness. The capstone'scheck()s verify the correctness of the HTTP response under load, which is a different thing. - Optimizing the app or the database when a threshold fails (fixing the slow query, adding an index, adding a cache) is outside this guide: it's the "after." The capstone leaves you standing right there —with a red gate and a p95 that crossed the SLO— and tells you where to continue, but it doesn't optimize.
- This is the last module. There's no M9 that "finishes" the capstone: lesson 8 is the complete delivery and the close of the guide.
In one sentence: this module is where you assemble and deliver the complete load test. Everything you see you already learned; what's new is joining it.
What the destination looks like (an executed preview)
So the map isn't only words, here's the end of the road, actually executed. The same load test (the quote→book scenario in smoke→load→stress stages, with the same thresholds) runs against two targets. First, against the healthy build /quote:
What to expect — with the fast endpoint, the whole run's p95 stays well below the 200 ms SLO; the gate passes and exits with code 0:
$ python3.14 loadtest.py http://127.0.0.1:PORT /quote green.json
...
THRESHOLD MEASURED RESULT
http_req_duration: p(95) < 200ms p(95) = 21.72ms PASS
http_req_failed: rate < 1.00% rate = 0.00% PASS
checks: rate > 99.00% rate = 100.00% PASS
--------------------------------------------------------------------------
metrics exported -> green.json
GATE: PASS (exit code 0)
$ echo $?
0
And now, the same test against the heavy pricing engine /quote_cpu, whose p95 spikes under the stress:
What to expect — the CPU work serialized by the GIL makes the p95 cross the SLO under the peak load; the gate fails and exits with code 1:
$ python3.14 loadtest.py http://127.0.0.1:PORT /quote_cpu red.json
...
THRESHOLD MEASURED RESULT
http_req_duration: p(95) < 200ms p(95) = 218.51ms FAIL
http_req_failed: rate < 1.00% rate = 0.00% PASS
checks: rate > 99.00% rate = 100.00% PASS
--------------------------------------------------------------------------
metrics exported -> red.json
GATE: FAIL (exit code 1)
$ echo $?
1
That's the whole capstone in two commands: the same test, green with the healthy load and red when the pricing engine gets heavy and the p95 crosses the limit under the stress —each with its real exit code, the one a CI pipeline would use to authorize or block the deploy—. (The exact numbers vary a bit between runs, because they depend on how the operating system distributes the time; what doesn't vary is the story: /quote passes the SLO with room to spare, /quote_cpu crosses it under the stress, and the gate catches it.) The rest of the lessons build this piece by piece up to the final delivery.
Common mistakes
Believing the capstone teaches something new. What happens: someone arrives here expecting one more concept and gets frustrated because "only" things get joined. Why it happens: learning a technique gets confused with knowing how to apply it fully. How to detect it: if you think integrating is "less" than learning, you haven't done the assembly exercise. How to fix it: understand that joining the seven pieces into a test that runs, measures, judges, and automates is a skill in itself —the one that's really used at work—. The symphony isn't "less" than the rehearsals; it's what they were for.
Expecting the test to always pass. What happens: someone designs the test to come out green and is surprised (or scared) when they see the gate red. Why it happens: "a test that passes" gets confused with "a good test." How to detect it: if your test can never fail, it isn't measuring a real limit. How to fix it: a good load test can fail —that's the point—. The red gate against /quote_cpu isn't an error: it's the test doing its job, catching that the p95 crossed the SLO under load. A gate that never goes red protects nothing.
Believing k6 or CI ran here. What happens: someone sees the .js script or the load.yml and cites it as "what this guide did." Why it happens: the k6 and GitHub Actions content looks very real. How to detect it: k6 isn't installed and git/gh are never run here; the executed stuff always comes with a python3.14 ... command. How to fix it: remember the rule —Python is run and cited; k6 and the YAML are labeled content, faithful to the docs—.
Exercises
Exercise 1 — From the piece to the module. For each capstone piece, say which module what you need to build it comes from. (a) The POST /quote → POST /book scenario with check(). (b) The stages: [...] smoke→load→stress. (c) The thresholds: { http_req_duration: ['p(95)<200'] }. (d) Exporting the metrics to results.json and the CI load.yml.
See solution
- (a) Modules 2 and 6: the
defaultfunction andhttp.post(M2), the correctnesscheck()s, the quote→book correlation, and thesleepwith jitter (M6). - (b) Module 4: the load profiles and the
stagesoption (ramp-up/steady/ramp-down, and the smoke/load/stress shapes). - (c) Module 5: the thresholds that turn a metric into pass/fail and the exit code that gates CI.
- (d) Module 7: exporting the result (
--out json) and running k6 in CI with theload.yml.
Each capstone piece is a previous module. Integrating is knowing which one contributes what.
Exercise 2 — Why /quote_cpu and not /quote for the red? The capstone runs the same test against two targets. (a) Why does the gate against /quote go green even under the stress stage? (b) Why does /quote_cpu go red? (c) What real situation does /quote_cpu model?
See solution
- (a)
/quoteonly computes a price (a multiplication and an integer division) and responds: it's so fast that, even with 80 concurrent VUs onlocalhost, its p95 stays in the tens of milliseconds, well below the 200 ms SLO. The system is healthy, so it passes. - (b)
/quote_cpudoes CPU work per request, and Python's GIL serializes that work between threads: with many clients at once (stress), the compute queue grows and the p95 spikes over 200 ms. The latency depends on the load, so the peak breaks it. - (c) It models an endpoint whose bottleneck is the computation: a pricing engine that does a heavy calculation, a function that got costly after a change, an operation that doesn't scale with concurrency. Cheap under low load, expensive under high —exactly what a stress test exists to discover—.
Exercise 3 — Green isn't "good," red isn't "bad." In the preview, the gate against /quote came out with $? = 0 and against /quote_cpu with $? = 1. (a) What does each number communicate to a CI pipeline? (b) Why is the red gate a success of the test, not a failure? (c) What would you do, as a team, faced with the red gate?
See solution
- (a)
0means "all fine, the pipeline can continue (and deploy)";1(or any ≠ 0) means "something failed, the pipeline stops and the deploy is blocked." It's the universal language of the exit code (M5). - (b) Because the test did its job: it detected that the p95 crossed the SLO under load and communicated it with an exit code that blocks the deploy —before a real user lived that slowness—. A test that catches a problem is a success; one that can never fail protects nothing.
- (c) Investigate why the p95 crossed (the app? the database? the network?) and optimize the bottleneck —which is outside this guide; it's the "after"—. The load test tells you that there's a problem and where to look; fixing it is the next job. The red gate is the beginning of that conversation, not the end.
Summary and next step
This module is the capstone: where the seven pieces you learned separately are assembled into a complete load test of the Reservo API and you deliver it. There's no new topic; what's new is joining it all. You saw the orchestra analogy (the sectional rehearsals = the modules; the symphony = the capstone), the four pieces you'll build (the k6 script as content, the canonical API as the target, the executable run in Python with its gate, the CI as content), the /quote_cpu endpoint we reuse from M5 to provoke the degradation, the environment rule (Python is run; k6 and CI are content), and the executed destination: the same test green against /quote (exit 0) and red against /quote_cpu (exit 1).
Before moving on you should be able to: name the four capstone pieces and which module each comes from; explain why /quote_cpu degrades under load and /quote doesn't; and understand why a red gate is the test working, not failing. What comes next, in lesson 2, is the first piece: the quote→book scenario with checks —the default function that is the test's heart—, written in k6 (content) and actually run in Python with its checks at 100%.
Resources
- k6 — Get started (write your first test) — the official walkthrough that builds a complete script with
http,check,sleep,options, andthresholds; the map of everything this capstone assembles. The source of the k6 content. - k6 — Test types (smoke, load, stress) — the official reference for the test types the capstone's profile traverses (smoke → load → stress); the foundation of lesson 3.
- Google SRE Book — Service Level Objectives — why a test is judged against an SLO and not in the abstract; the criterion behind the capstone's thresholds (lesson 4).
concurrent.futures.ThreadPoolExecutor— Python documentation — the thread pool the generator models the VUs with (one worker per VU) in the executable run. The engine of what's actually run.