Webapp Loadtest
Time-bounded HTTP load test with Locust. 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
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:
- 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.
- Reachability test with 1 user, 10 seconds,
--headless. Confirm Locust sees the target.
- 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
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:
- Summary table — per endpoint: total requests, failures, failure rate, p50, p95, p99, max.
- Overall — total requests, total failures, avg RPS, peak RPS, peak p95.
- Errors breakdown — unique error types + count.
- 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»
- 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
- Never run without explicit duration and user count. No unbounded tests.
- Never run against a target the user hasn't explicitly named.
<target> must be typed, not inferred.
- Always print the billing warning for serverless targets (Vercel, Cloudflare Workers, AWS Lambda, GCP Cloud Run) before starting.
- 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.
- Stop if error rate crosses 20% or target returns consecutive 5xx for > 30 s. Print what happened, skip the rest of the planned duration.
- Never load-test an endpoint you haven't smoke-tested first. A broken endpoint doesn't reveal anything under load.
- 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 |
1---2name: loadtest3description: 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).4---56# Webapp Loadtest78Time-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.910**Default language:** match user. **Output:** `{cwd}/pentest-output/{target-slug}-{YYYY-MM-DD}/loadtest/`.1112**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.1314---1516## Phase 0 — Scope1718Ask or confirm:1920- **Target URL** — base URL, e.g. `https://api.example.com`21- **Endpoints to hit** — list of paths + methods + bodies. Default: just `GET /` if user didn't say.22- **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).23- **Load profile** — concurrent users + ramp-up + duration. Sane defaults:24 - `--users 20 --spawn-rate 2 --run-time 2m` for a baseline25 - User can override with `--users 100 --spawn-rate 10 --run-time 5m` etc.26- **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.27- **Billing consent** — confirm the target can afford this. If Vercel / serverless is involved, say it out loud.2829## Phase 1 — Workspace3031```bash32slug=$(echo "<target-host>" | tr -cd 'a-z0-9-')33day=$(date +%Y-%m-%d)34workdir="${PWD}/pentest-output/${slug}-${day}/loadtest"35mkdir -p "${workdir}"36echo 'secrets.env37*.local.*' > "${workdir}/.gitignore"38```3940## Phase 2 — Generate Locustfile4142Render `references/locustfile-template.py` into `${workdir}/locustfile.py`. Fill in:4344- Base URL45- Endpoint list (one `@task(weight=N)` per endpoint)46- Auth injection (headers or cookies)47- Optional: warm-up request on `on_start`4849For Telegram mini apps, the template supports the `x-telegram-init-data` header. Read the initData from `secrets.env`, same format as pentest.5051## Phase 3 — Preflight5253Before launching the real test:54551. **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.562. **Reachability test** with 1 user, 10 seconds, `--headless`. Confirm Locust sees the target.573. **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.5859## Phase 4 — Run6061```bash62cd "${workdir}"63# WAIT_MIN/WAIT_MAX shape per-user RPS — see Phase 0.64# Set both to 0 to disable the cap (advanced).65WAIT_MIN="${WAIT_MIN:-1}" WAIT_MAX="${WAIT_MAX:-2}" \66locust \67 -f locustfile.py \68 --headless \69 --host "<target>" \70 --users "${USERS:-20}" \71 --spawn-rate "${SPAWN_RATE:-2}" \72 --run-time "${DURATION:-2m}" \73 --csv=stats \74 --html=report.html \75 --only-summary \76 --loglevel WARNING77```7879Capture the exit code. Non-zero = Locust had errors or the target became unreachable.8081## Phase 5 — Analyze + report8283Parse `stats_stats.csv` and `stats_failures.csv`. Generate `${workdir}/REPORT.md`:84851. **Summary table** — per endpoint: total requests, failures, failure rate, p50, p95, p99, max.862. **Overall** — total requests, total failures, avg RPS, peak RPS, peak p95.873. **Errors breakdown** — unique error types + count.884. **Interpretation** — Claude's comment:89 - «At X RPS, the endpoint stayed under 200 ms p95 with 0% errors — healthy»90 - «Errors started at minute N when concurrency hit M — look at the backend logs»91 - «p95 latency grew linearly with load — backend is likely CPU-bound or waiting on DB»925. **Next steps suggestions** — if error rate > 5% or p95 > 2s, suggest narrowing the load or checking backend logs; don't dial up load.9394Reference the locustfile and raw CSVs by absolute path so the user can re-run later.9596## Phase 6 — Cleanup9798Delete nothing automatically. The workdir stays — user reviews and cleans up when done.99100If the run caused observable target issues (lots of 502s, slow recovery), print:101> **Warning:** target returned many 5xx responses during the test. Give the service a few minutes before running again. Consider reducing --users.102103---104105## When Locust lite is NOT enough106107- **Production-scale load** (thousands of concurrent users, distributed traffic from many geos)108- **WebSocket / long-running connection testing**109- **Complex auth flows with session refresh**110- **Rate-limit evasion via proxy rotation**111112For 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.113114---115116## Rules1171181. **Never** run without explicit duration and user count. No unbounded tests.1192. **Never** run against a target the user hasn't explicitly named. `<target>` must be typed, not inferred.1203. **Always** print the billing warning for serverless targets (Vercel, Cloudflare Workers, AWS Lambda, GCP Cloud Run) before starting.1214. **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.1225. **Stop if** error rate crosses 20% or target returns consecutive 5xx for > 30 s. Print what happened, skip the rest of the planned duration.1236. **Never** load-test an endpoint you haven't smoke-tested first. A broken endpoint doesn't reveal anything under load.1247. **No Claude-only APIs** (`TaskCreate`, `TeamCreate`, `AskUserQuestion`) — runs in Claude Code and Codex.125126---127128## References129130| File | Purpose |131|------|---------|132| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/locustfile-template.py` | Jinja-style Locust scenario with auth slots |133| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/report-template.md` | REPORT.md skeleton for loadtest findings |134| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/auth-patterns.md` | Bearer / cookie / Telegram-initData injection recipes |135| `${CLAUDE_PLUGIN_ROOT:-.}/skills/loadtest/references/jmeter-heavy-setup.md` | Pointer to a full distributed JMeter setup for heavy-duty load |