# Loadtest

> Lightweight HTTP load testing with Locust. Use when the user asks to run a load test / stress test / нагрузочное тестирование / check RPS a web app can handle / measure latency under load / simulate N users. Generates a Locustfile from a template, runs the test for a bounded duration, outputs JSON stats + a markdown report with p50/p95/p99 latency, error rate, and per-endpoint breakdown. Trigger keywords: load test, loadtest, нагрузка, нагрузочное тестирование, stress test, simulate N users, RPS, requests per second, locust. NOT for security probing (use pentest instead) and NOT for real traffic simulation at production scale (link to the distributed JMeter setup for that).

- Skill: `oxi-717/loadtest` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add oxi-717/loadtest`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oxi-717/loadtest/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: OXI-717 (https://skillmd.com/u/oxi-717)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oxi-717/loadtest

---


# Webapp Loadtest

Time-bounded HTTP load test with [Locust](https://locust.io). Generates a scenario file from a template, runs for the duration the user names, produces a markdown report.

**Default language:** match user. **Output:** `{cwd}/pentest-output/{target-slug}-{YYYY-MM-DD}/loadtest/`.

**Never run without a billing-awareness check.** Serverless targets (Vercel, Cloudflare Workers, AWS Lambda) charge per request. A load test that runs for 10 minutes at 50 RPS = 30,000 requests the target owner pays for. Confirm the budget before starting.

---

## Phase 0 — Scope

Ask or confirm:

- **Target URL** — base URL, e.g. `https://api.example.com`
- **Endpoints to hit** — list of paths + methods + bodies. Default: just `GET /` if user didn't say.
- **Auth** — none / bearer token / cookie / Telegram initData / custom header. If auth is needed, ask user for the artifact once and store in `secrets.env` (gitignored).
- **Load profile** — concurrent users + ramp-up + duration. Sane defaults:
  - `--users 20 --spawn-rate 2 --run-time 2m` for a baseline
  - User can override with `--users 100 --spawn-rate 10 --run-time 5m` etc.
- **RPS cap** — enforced inside the **generated locustfile** via `wait_time = between(WAIT_MIN, WAIT_MAX)`. Defaults `WAIT_MIN=1, WAIT_MAX=2` → ~0.5–1 req/s/user → ~10–20 RPS at 20 users. Locust itself has no `--no-rps-cap` flag; to remove the cap, the user passes `WAIT_MIN=0 WAIT_MAX=0` as env vars and the locustfile switches to `constant(0)` wait.
- **Billing consent** — confirm the target can afford this. If Vercel / serverless is involved, say it out loud.

## Phase 1 — Workspace

```bash
slug=$(echo "<target-host>" | tr -cd 'a-z0-9-')
day=$(date +%Y-%m-%d)
workdir="${PWD}/pentest-output/${slug}-${day}/loadtest"
mkdir -p "${workdir}"
echo 'secrets.env
*.local.*' > "${workdir}/.gitignore"
```

## Phase 2 — Generate Locustfile

Render `references/locustfile-template.py` into `${workdir}/locustfile.py`. Fill in:

- Base URL
- Endpoint list (one `@task(weight=N)` per endpoint)
- Auth injection (headers or cookies)
- Optional: warm-up request on `on_start`

For Telegram mini apps, the template supports the `x-telegram-init-data` header. Read the initData from `secrets.env`, same format as pentest.

## Phase 3 — Preflight

Before launching the real test:

1. **Single smoke request** to each endpoint with the final config. Confirm 2xx (or documented expected status). If a single request returns 5xx, the load test will just amplify a broken endpoint — fix or exclude it.
2. **Reachability test** with 1 user, 10 seconds, `--headless`. Confirm Locust sees the target.
3. **Billing check-in** — print «Starting load test: N users, T duration, cap R RPS. Proceed? Press Ctrl-C in the next 5 seconds to abort.» and `sleep 5`. This is the soft abort gate.

## Phase 4 — Run

```bash
cd "${workdir}"
# WAIT_MIN/WAIT_MAX shape per-user RPS — see Phase 0.
# Set both to 0 to disable the cap (advanced).
WAIT_MIN="${WAIT_MIN:-1}" WAIT_MAX="${WAIT_MAX:-2}" \
locust \
  -f locustfile.py \
  --headless \
  --host "<target>" \
  --users "${USERS:-20}" \
  --spawn-rate "${SPAWN_RATE:-2}" \
  --run-time "${DURATION:-2m}" \
  --csv=stats \
  --html=report.html \
  --only-summary \
  --loglevel WARNING
```

Capture the exit code. Non-zero = Locust had errors or the target became unreachable.

## Phase 5 — Analyze + report

Parse `stats_stats.csv` and `stats_failures.csv`. Generate `${workdir}/REPORT.md`:

1. **Summary table** — per endpoint: total requests, failures, failure rate, p50, p95, p99, max.
2. **Overall** — total requests, total failures, avg RPS, peak RPS, peak p95.
3. **Errors breakdown** — unique error types + count.
4. **Interpretation** — Claude's comment:
   - «At X RPS, the endpoint stayed under 200 ms p95 with 0% errors — healthy»
   - «Errors started at minute N when concurrency hit M — look at the backend logs»
   - «p95 latency grew linearly with load — backend is likely CPU-bound or waiting on DB»
5. **Next steps suggestions** — if error rate > 5% or p95 > 2s, suggest narrowing the load or checking backend logs; don't dial up load.

Reference the locustfile and raw CSVs by absolute path so the user can re-run later.

## Phase 6 — Cleanup

Delete nothing automatically. The workdir stays — user reviews and cleans up when done.

If the run caused observable target issues (lots of 502s, slow recovery), print:
> **Warning:** target returned many 5xx responses during the test. Give the service a few minutes before running again. Consider reducing --users.

---

## When Locust lite is NOT enough

- **Production-scale load** (thousands of concurrent users, distributed traffic from many geos)
- **WebSocket / long-running connection testing**
- **Complex auth flows with session refresh**
- **Rate-limit evasion via proxy rotation**

For these: see `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/jmeter-heavy-setup.md` — describes the patterns (token refresh, cache-busting, proxy rotation, baseline comparison, Grafana dashboards) and points at the reference implementation. The portable patterns apply to any JMeter + Docker stack, public or private.

---

## Rules

1. **Never** run without explicit duration and user count. No unbounded tests.
2. **Never** run against a target the user hasn't explicitly named. `<target>` must be typed, not inferred.
3. **Always** print the billing warning for serverless targets (Vercel, Cloudflare Workers, AWS Lambda, GCP Cloud Run) before starting.
4. **Default cap** ~10–20 RPS at 20 users (via `WAIT_MIN=1 WAIT_MAX=2` in the generated locustfile). To exceed, the user explicitly sets `WAIT_MIN=0 WAIT_MAX=0` in the env. Document this when running, don't silently disable.
5. **Stop if** error rate crosses 20% or target returns consecutive 5xx for > 30 s. Print what happened, skip the rest of the planned duration.
6. **Never** load-test an endpoint you haven't smoke-tested first. A broken endpoint doesn't reveal anything under load.
7. **No Claude-only APIs** (`TaskCreate`, `TeamCreate`, `AskUserQuestion`) — runs in Claude Code and Codex.

---

## References

| File | Purpose |
|------|---------|
| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/locustfile-template.py` | Jinja-style Locust scenario with auth slots |
| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/report-template.md` | REPORT.md skeleton for loadtest findings |
| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/auth-patterns.md` | Bearer / cookie / Telegram-initData injection recipes |
| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/jmeter-heavy-setup.md` | Pointer to a full distributed JMeter setup for heavy-duty load |

