Stress & Load Testing (Locust)
6.1 The five tests
They answer different questions, so they aren’t interchangeable:
| Test | Question | How |
|---|---|---|
| Load | Does it meet its targets at the expected peak? | Run at the expected peak number of users for a set time |
| Stress | Where does it break? | Keep increasing users past the peak until targets are missed |
| Spike | What happens on a sudden jump? | Jump to a high load, hold briefly, drop back |
| Soak | Does it survive long, steady use? | Normal load for several hours (typically 2–8), watching for slow problems like memory growth |
| Recovery | Does it come back by itself? | After a stress or spike run, check it returns to normal without a restart |
6.2 Where to run
- Never in production. Use a test environment that’s as close to production as possible.
- For our OCR functions, use the mock OCR engine unless the test is specifically about the real OCR service. A load test against a paid service costs real money.
- Agree the test time with the owning team, because it can slow the environment down for others.
6.3 Write the pass criteria before the run
Every load test case states its targets up front, agreed with the developer or owning team. Example:
**Summary:** PERF-OCRUPLOAD-001 — Upload endpoint at expected peak
**Category:** Performance · **Priority:** P1
**Load:** 50 users, 10 minutes
**Pass criteria:**
- 95% of uploads respond within 2 seconds (p95 < 2 s)
- Error rate below 1%
- Every uploaded job reaches Finished within 5 minutes after the test endsWithout agreed criteria, a load test gives numbers but no verdict.
For async systems like the OCR functions, measure two things: how fast the upload responds, and how long until the jobs actually finish. A fast upload with a queue that never catches up still fails.
Stress tests pass when the breaking point is above the agreed minimum (for example, 2× the expected peak) and the system recovers by itself. Always record the breaking point: the number of users, and the first thing that failed.
6.4 How a test looks
locustfile.py:
from pathlib import Path
from locust import HttpUser, task, between
PDF = Path(r"C:\temp\invoice.pdf").read_bytes()
class OcrUploadUser(HttpUser):
wait_time = between(1, 3)
@task
def upload_pdf(self):
self.client.post(
"/api/HttpTriggerOcrEtaxAsync",
files={"file": ("invoice.pdf", PDF, "application/pdf")},
data={"service": "load-test"},
name="upload",
)Run it (50 users, adding 5 per second, for 10 minutes):
python -m pip install locust
locust -f locustfile.py --headless -u 50 -r 5 -t 10m --host http://<test environment> --csv results\ocruploadWithout --headless, Locust opens a web dashboard at http://localhost:8089,
where you can watch the charts live and change the number of users during the run.
This is good for stress and spike tests, and for demos.
6.5 Recording the result in Kiwi
On the test case’s execution, set PASSED or FAILED against the criteria, and add:
- Result numbers: p95 response time, error rate, requests per second, and for stress tests, the breaking point.
- Setup: environment, app version, number of users, duration, and the data used.
- Comparison: the same numbers from the previous release’s run.
- Files: attach the Locust CSV results or a screenshot of the charts.
A result that’s much worse than the previous release is a bug, even if it still meets the criteria.
6.6 When to run
| Test | When |
|---|---|
| Load | Before a critical endpoint’s first release, and when a change touches its queries or dependencies |
| Stress, soak | Before a major release, or before an expected traffic peak (e.g. tax filing deadline) |
| Spike | When traffic genuinely comes in bursts |