Point k6 at your monolith and watch it bend under load. The pain you measure today gives meaning to every concept in the next 23 lessons.
By the end of this hour, you'll have run progressive load tests (10 → 50 → 200 virtual users) against Wassal v0, captured the latency and error metrics in a results file, and tagged the codebase as lesson-01-pain-documented. You'll have measured evidence of where the monolith starts to fail — not just a feeling.
🎯 Today's goal: progressive load tests captured in a results file and the codebase tagged
lesson-01-pain-documented. Six sequential tasks, ~60 minutes.
Task 1 — Install k6 (5 min)
k6 is an open-source load testing tool from Grafana Labs. Scripts are written in JavaScript, runs are super fast, and the summary output is exactly what we need.
Install via winget:
winget install k6.k6 --accept-source-agreements --accept-package-agreements
Open a fresh PowerShell window (winget edits PATH; the old window won't see k6 until you reopen), then verify:
k6 version
💡 winget failed? Use the Docker image instead — no install needed:
docker run --rm -i --network=host grafana/k6 versionReplace every
k6 ...command below withdocker run --rm -i --network=host -v ${PWD}:/app -w /app grafana/k6 ....
✅ Done when: k6 version prints something like k6 v0.x.x.
Task 2 — Create the Load Test Script (10 min)
Create the folder structure for load tests at the repo root:
cd D:\books\distributed-system\Wassal
New-Item -ItemType Directory -Path scripts\load-tests -Force | Out-Null
Create the k6 script — it ramps from 10 → 50 → 200 virtual users while hitting /:
// scripts/load-tests/wassal-v0-stress.js
import http from 'k6/http';
import { check, sleep } from 'k6';
// Progressive load: warm up, then climb in 3 plateaus
export const options = {
stages: [
{ duration: '30s', target: 10 }, // ramp up to 10 vusers
{ duration: '60s', target: 10 }, // hold for 60s (baseline)
{ duration: '30s', target: 50 }, // ramp to 50
{ duration: '60s', target: 50 }, // hold for 60s (medium)
{ duration: '30s', target: 200 }, // ramp to 200
{ duration: '60s', target: 200 }, // hold for 60s (stress)
{ duration: '30s', target: 0 }, // ramp down
],
// These are thresholds, NOT guarantees. We expect them to fail.
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // less than 1% errors
},
};
export default function () {
const baseUrl = __ENV.BASE_URL || 'http://localhost:5000';
const res = http.get(`${baseUrl}/`);
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1); // simulate user think time
}
💡 Why these thresholds? They're industry-standard SLAs for a web app (p95 < 500ms, error rate < 1%). They WILL fail today — and that failure is the whole point.
✅ Done when: the file exists at scripts/load-tests/wassal-v0-stress.js.
Task 3 — Start Wassal v0 on HTTP (and Smoke-Test) (5 min)
Run the app on the plain HTTP profile so k6 doesn't have to deal with the dev HTTPS certificate:
cd D:\books\distributed-system\Wassal
dotnet run --project src/Wassal.Monolith --launch-profile http
Look for a line like Now listening on: http://localhost:5000 in the output. Leave this window running.
Open a second PowerShell window and do a quick warmup with 5 vusers for 10 seconds to make sure everything's wired up:
cd D:\books\distributed-system\Wassal
k6 run --vus 5 --duration 10s scripts/load-tests/wassal-v0-stress.js
💡 Port not 5000? Check the URL the app printed (could be 5051 or similar based on your
launchSettings.json). If it's different, set the environment variable before each k6 call:$env:BASE_URL = "http://localhost:5051"
✅ Done when: the smoke test finishes with 0 errors and the app window is still running.
Task 4 — Run the Full Stress Test (and Save the Output) (15 min)
The full script takes ~5 minutes to complete. Run it and pipe the output to a file so you can grep through it later:
k6 run scripts/load-tests/wassal-v0-stress.js | Tee-Object -FilePath scripts\load-tests\run-output.txt
Watch the live output while it runs. You'll see something like this every few seconds:
running (3m05.4s), 050/050 VUs, 1842 complete and 0 interrupted iterations
default [==========>------] 050/200 VUs 3m05.4s/5m00.0s
When it finishes, k6 prints a summary block. The key numbers to capture:
http_req_duration ........ avg, p(90), p(95), p(99)http_req_failed .......... rateiteration_duration ....... avgiterations ............... total, /s- Threshold checks at the bottom — note which ones FAILED (marked with ✗).
💡 Watch the second window too. The
dotnet runwindow will be screaming logs. Notice the request bursts, any warnings, and whether SQL Server connection errors appear.
✅ Done when: scripts\load-tests\run-output.txt exists, the run finished, and you noted which thresholds FAILED.
Task 5 — Document Your Findings (results-v0.md) (15 min)
Capture what you saw in a results file. This is your before snapshot — every future "after" run will be compared against this.
# scripts/load-tests/results-v0.md
# Wassal v0 — Baseline Stress Test Results
**Date:** 2026-05-20
**Tag:** `v0-monolith-baseline` / `lesson-01-before`
**Tool:** k6 v0.x.x
**Environment:** local dev, SQL Server `(local)`, Wassal.Monolith on Kestrel
## Setup
- Endpoint tested: `GET /` (the restaurants index page)
- Stages: 10 vusers (warm) → 50 (medium) → 200 (stress)
- Script: `scripts/load-tests/wassal-v0-stress.js`
## Results
| Phase | VUs | Avg latency | p95 latency | Error rate | Iterations/s |
|----------------|-----|-------------|-------------|------------|--------------|
| Warm | 10 | ___ ms | ___ ms | __ % | __ |
| Medium | 50 | ___ ms | ___ ms | __ % | __ |
| Stress | 200 | ___ ms | ___ ms | __ % | __ |
## Thresholds
- `p(95) < 500ms`: **___** (PASS / FAIL)
- `error rate < 1%`: **___** (PASS / FAIL)
## What I observed
(fill in what you actually saw — examples below)
1. At 10 vusers: everything green, p95 around __ms.
2. At 50 vusers: p95 climbed to __ms, no errors yet.
3. At 200 vusers: ___________
4. The `dotnet run` window showed: ___________
## Where the pain came from
Pick the ones that match what you saw:
- [ ] SQL Server connection pool exhausted (look for "timeout" errors)
- [ ] Kestrel request queue overflowed (slow latency, no errors)
- [ ] CPU saturated (check Task Manager during stress)
- [ ] Memory grew unbounded (check working set)
- [ ] Razor view rendering became a bottleneck
- [ ] Other: ___________
## Concepts that will fix each pain
| Pain observed | Concept that addresses it | Lesson |
|---|---|---|
| Latency under load | Caching (Redis) | Lesson 6+ |
| Single point of failure | Replicas + load balancer | Lesson 17 |
| One service crashes all | Microservices + bulkheads | Lesson 20, 22 |
| No visibility into the crash | Observability (logs, metrics, traces) | Lesson 24 |
| Connection pool exhausted | Connection pooling + async patterns | Lesson 4–7 |
Fill in the blanks with your actual numbers from Task 4. Even if some metrics look fine — record them. Tomorrow's "after" will be compared to today's truth.
✅ Done when: results-v0.md has real numbers from your run and your honest observations.
Task 6 — Commit + Tag the Pain (5 min)
Stop the running app (Ctrl+C in the dotnet window) and commit your work:
cd D:\books\distributed-system\Wassal
git add scripts/ docs/sessions/Day-02-Stress-Test-Wassal-v0.html
git status # verify what's staged
git commit -m "test: Lesson 1 — stress test reveals pain (k6 baseline)"
git tag lesson-01-pain-documented
Verify the tag:
git tag
# expected output:
# lesson-01-before
# lesson-01-pain-documented
# v0-monolith-baseline
💡 Why a separate tag for the pain?
lesson-01-beforemarks the code state.lesson-01-pain-documentedmarks the moment you have evidence of the pain. Tomorrow's "after" tag will be compared against this one — and the diff will include both code changes AND new measurements.
✅ Done when: 3 tags exist locally and git log --oneline shows the new commit.
🔥 The 6 Pains You'll Probably Witness
Don't worry if you don't see all of them — local hardware varies. Even one or two is enough to build intuition.
| # | Pain |
|---|---|
| Pain #1 | p95 latency climbs from ~50ms to 500ms+ under stress |
| Pain #2 | SQL Server connection pool exhaustion (errors at 200 vusers) |
| Pain #3 | Kestrel request queue grows; some requests time out |
| Pain #4 | If you stop SQL Server mid-run: the whole site dies (SPOF) |
| Pain #5 | No metrics, no traces, no insight into where time is spent |
| Pain #6 | Recovery is "restart everything" — no graceful degradation |
📋 Quick Checklist
| # | Task | Time | Done When |
|---|---|---|---|
| 1 | Install k6 | 5 min |
k6 version prints v0.x.x |
| 2 | Create the k6 script | 10 min |
wassal-v0-stress.js exists |
| 3 | Start app on HTTP + smoke test | 5 min | 5-vuser smoke run: 0 errors |
| 4 | Run the full stress test | 15 min |
run-output.txt saved |
| 5 | Document findings | 15 min |
results-v0.md filled in |
| 6 | Commit + tag | 5 min | 3 tags exist locally |
🎓 Tomorrow's Preview — Lesson 2 (Article #4): OSI & Why Networks Lie
With the pain measured and tagged, the next session shifts gears: we'll dive into the network layer the monolith depends on. Why does TCP have a 3-way handshake? What's the cost of TLS? Why does a connection close in TIME_WAIT keep the socket alive for minutes? We'll inspect a real Wassal request with Wireshark and start to understand the foundations the next 22 lessons build on.
Part of the **Fundamentals of Distributed Systems* series — building Wassal, a distributed food-delivery lab, one concept at a time.*

Top comments (0)