Skip to content
Back to skills

Load Test

ASecurity

Design and run a load / stress / spike / soak campaign against an SLA on a prod-parity env, then interpret it — throughput, latency percentiles, breakpoint/knee, saturation — and return a PASS/FAIL verdict + headroom. Distinct from profile-endpoint (diagnoses ONE slow subject) and capacity-planner (analytical estimation); this is the empirical whole-system validation.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentspythongojavabashnodeexpresstestingapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill load-test --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Load Test?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Load Test
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-load-test/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-load-test)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: load-test
description: Design and run a load / stress / spike / soak campaign against an SLA on a prod-parity env, then interpret it — throughput, latency percentiles, breakpoint/knee, saturation — and return a PASS/FAIL verdict + headroom. Distinct from profile-endpoint (diagnoses ONE slow subject) and capacity-planner (analytical estimation); this is the empirical whole-system validation.
kind: skill
pack: performance
allowed-tools: [Read, Grep, Glob, Bash]
---

# load-test

Design a load campaign against an SLA, drive it on a prod-parity env, read the result: throughput, latency percentiles, the breakpoint, the saturated resource. Ship a verdict, not a vibe.

## Premise

An SLA with no load test behind it is a promise no one has kept. This skill owns the campaign that either keeps it or breaks it — and the number that proves which.

**Cite-or-halt.** Every capacity claim cites the run config (tool, model, stages, rate, duration, env) AND the measured percentile / throughput it rests on. "It should handle the load" is a vibe; `p99 = 780ms at 4.2k rps with 0.3% errors on the staging fleet` is a finding. A single-VU "quick load" number quoted as capacity is the failure mode this skill exists to kill — one virtual user in a loop measures round-trip latency of an idle system, not what happens at peak.

**Boundary — `profile-endpoint` (sibling skill).** `profile-endpoint` diagnoses ONE slow subject under quick load — it points the flamegraph + slow-query log at a single endpoint and asks *where does the time go here*. `load-test` designs a CAMPAIGN validating whole-system throughput and headroom against an SLA — many paths, realistic mix, ramp to breakpoint, asks *does the system meet the SLA and where does it fall over*. Profiling explains a bottleneck; this skill finds and gates on it. When a campaign exposes a knee, hand the subject at the knee to `profile-endpoint` for the root cause.

**Boundary — `capacity-planner` (distributed-systems pack agent).** `capacity-planner` does ANALYTICAL back-of-envelope estimation — Little's Law, `peak QPS × payload`, single-node ceilings — proving on paper whether a design *should* fit before it's built. `load-test` does EMPIRICAL validation — it drives real traffic at the real system and measures what actually happens. They are complementary, not overlapping: the planner predicts the breakpoint, this skill confirms or refutes it against reality. A prediction that was never load-tested is a hope; a load test with no capacity model is a number with no expected value to compare against. Run the planner first for the target, then this skill to validate it.

## When to run

- Before a **launch**, a **capacity change** (new region, bigger fleet, autoscale retune), or a **big feature** on a hot path.
- To **validate an SLA/SLO** — you have a written `p99 < X at Y rps with <Z% errors` and need to prove or break it.
- After a suspected capacity regression, to re-establish the breakpoint.
- NOT on every CI run. A real load test needs a **prod-parity environment** (same instance sizes, same DB tier, realistic data volume, representative topology) and minutes of sustained traffic — it does not belong in the per-commit gate. A tiny smoke-load in CI is fine as a canary; do not mistake it for the campaign.

## Test taxonomy

Pick the campaign type by the question you need answered. Each has a goal and an explicit pass/fail.

| Type | Question it answers | Profile | Pass / fail |
|---|---|---|---|
| **Load** | Does it meet the SLA at *expected peak*? | Hold at target rps for a sustained window | PASS = p99 under SLA at target rps with errors < budget |
| **Stress** | *Beyond* peak, where does it break? | Ramp past peak until SLA violates or errors spike | Locate the rps where p99 or error-rate crosses the line |
| **Spike** | Does it survive a *sudden surge* and recover? | Jump from baseline to N× instantly, hold, drop | PASS = absorbs the spike and returns to baseline latency within the recovery window (no lingering queue) |
| **Soak / endurance** | Does it degrade over *hours*? | Hold moderate load for hours | PASS = flat latency + flat memory + flat conn-count; leak/creep = fail |
| **Breakpoint / capacity** | Where is the *knee*? | Slow ramp to find where latency turns non-linear | Report the knee (max sustainable rps) + the resource that binds there |

Load answers "are we safe at peak"; breakpoint answers "how much headroom past peak". Run both before a launch — the gap between them is your margin.

## Adapt to the codebase

Drive the project's existing load tool; do not import a new one for the campaign. Detect it (repo `load/`, `k6/`, `*.jmx`, `locustfile.py`, `*.scala`, `artillery.yml`, CI perf stage), then match the campaign to what the tool expresses.

| Tool | Scripting model | Percentile reporting | Ramp / stages | Open-model support |
|---|---|---|---|---|
| **k6** | JS scenarios | p90/p95/p99 in summary + thresholds | `stages` (ramping-vus) | `ramping-arrival-rate` / `constant-arrival-rate` |
| **Gatling** | Scala/Java DSL | full percentile HTML report | `rampUsers` / `during` | `constantUsersPerSec` / `rampUsersPerSec` |
| **Locust** | Python tasks | p50…p99 web UI + CSV | `LoadTestShape` / step-load | `constant_pacing` (approx); closed by default |
| **JMeter** | XML thread groups (GUI/CLI) | Aggregate + percentiles listener | thread-group ramp-up | Throughput Shaping Timer (arrival-rate approx) |
| **Artillery** | YAML phases | p95/p99 in report | `phases` with `arrivalRate` | native `arrivalRate` (open by default) |
| **`wrk` / `vegeta`** | HTTP microbench CLI | `wrk`: coarse; `vegeta`: full HDR percentiles | `wrk` fixed; `vegeta -rate` ramps | `vegeta` is open (`-rate`); `wrk` is closed (fixed conns) |
| **Cloud** (BlazeMeter / k6 Cloud / Gatling Enterprise) | wraps the above | dashboards + trends | distributed geo-load | inherits the underlying engine's model |

`wrk`/`vegeta` are for a fast HTTP microbench of one route; k6 / Gatling / Locust / Artillery for a multi-scenario campaign with a realistic mix. Whatever the tool, the report MUST expose p95/p99 and per-stage throughput — a tool that only prints an average cannot gate an SLA.

## Procedure / methodology

1. **Model the workload from prod traffic shape** — a realistic **request mix** (weight each endpoint by its real share), **think-time** between a user's actions, and the **arrival pattern** (diurnal, bursty) taken from access logs / APM, not invented. A hammer loop hitting one route at zero think-time tests that route's ceiling, not your system's behaviour.
2. **Establish env parity** — run against a prod-like environment: same instance class, same DB tier + connection limits, realistic data volume, representative caches/queues. Load-testing a laptop (or empty tables) is worthless — the numbers describe the laptop. Record the env in the run config so the result is interpretable.
3. **Define the SLA gate up front** — write it before the run: `PASS = p99 < X ms at Y rps with < Z% errors`. A gate defined after seeing the numbers is not a gate.
4. **Warm up, then measure** — discard an initial low-load burst so JIT, connection pools, and caches reach steady state; only the post-warm-up window counts (mirrors `performance-principles`: warm-up + variance + p50/p95/p99).
5. **Choose the load model — open vs closed (the critical call).**
   - **Closed model** (fixed VUs): N virtual users each fire, wait for the response, then fire again. Throughput is *bottlenecked by the system* — when it slows, VUs pile up waiting and offered load automatically drops. Good for modelling a fixed connection pool.
   - **Open model** (arrival rate): requests arrive at Y/sec **regardless** of whether prior ones finished — like real internet traffic. When the system slows, the queue grows and latency reveals the true pain.
   - For an SLA gate, prefer the **open / arrival-rate** model. A closed model masks saturation (see gotchas: coordinated omission) and answers a different question.
6. **Ramp in stages** — step or ramp the rate (e.g. 25% → 50% → 100% → 150% of target), holding each stage long enough for a stable percentile read.
7. **Measure the full vector per stage** — p50 / p95 / p99 latency, throughput (rps), error-rate, AND server-side **saturation**: CPU, memory, connection-pool utilisation, DB connections / lock waits, queue depth. Latency alone tells you *that* it hurt; saturation tells you *what* ran out.
8. **Find the breakpoint (the knee)** — the rps where latency stops rising linearly and turns vertical (or errors cross the budget). Below the knee is capacity; the knee itself is the ceiling; above it is collapse. Name the resource saturated at the knee — that is the binding constraint.
9. **Compute headroom** — `(breakpoint rps − expected peak rps) / expected peak rps`. This is the margin the launch has before it needs more capacity.

## Output

A literal report. Every claim traces to the run config and a measured number.

```
## Load campaign — <system / release>  ·  <load|stress|spike|soak|breakpoint>

### Run config
Tool: k6 0.5x · model: ramping-arrival-rate (OPEN) · env: staging (prod-parity: 3× c5.xlarge, db.r6g.large, 8M-row dataset)
Workload: prod mix (browse 60% / search 25% / checkout 15%), think-time 3–8s, warm-up 60s discarded
Stages: 1k → 2k → 4k → 6k rps, 5m each

### SLA gate
PASS = p99 < 500ms at 4000 rps with < 0.5% errors  →  RESULT: PASS

### Per-stage results
| Stage (rps) | p50 | p95 | p99 | throughput | error-rate | saturation |
|---|---|---|---|---|---|---|
| 1000 | 40ms | 90ms  | 140ms | 1000 | 0.0%  | CPU 28%, pool 20% |
| 2000 | 55ms | 150ms | 240ms | 2000 | 0.0%  | CPU 51%, pool 44% |
| 4000 | 92ms | 310ms | 470ms | 3980 | 0.3%  | CPU 82%, pool 78% |  ← SLA target, PASS
| 6000 | 480ms| 2.1s  | 4.8s  | 4600 | 7.2%  | pool 100%, DB conns maxed |  ← past knee

### Breakpoint
Knee at ~4600 rps. Binding constraint: DB connection pool exhaustion (100% util, requests queue for a connection).
Above the knee, throughput plateaus at 4600 while latency runs away — classic saturation.

### Headroom
Expected peak 3000 rps · breakpoint ~4600 rps · headroom ≈ +53% over peak.

### Handoff (root cause is NOT this skill's job)
- Connection-pool saturation at the knee → @performance-optimizer (pool sizing / query time) + @caching-architect (offload reads).
- If the bottleneck is one endpoint → skill:profile-endpoint on that route for the flamegraph.
- If the model says it shouldn't bind yet → reconcile with @capacity-planner's ledger.

### Verdict: PASS — meets SLA at target with +53% headroom; connection pool binds first past the knee.
```

Closure verbs: **PASS / FAIL** the gate, **breakpoint** named with its bound resource, **headroom** quantified, **handoff** filed to the owner of the fix.

## False positives / gotchas

- **Coordinated omission (the fixed-VU trap)** — the term and the analysis are Gil Tene's ("How NOT to Measure Latency", the HdrHistogram work). A closed model hides latency under saturation: when the server stalls, VUs simply wait to send their *next* request, so the requests that *would* have been slow are never issued — the histogram omits exactly the pain you were measuring. Reported p99 looks fine while real users time out. Prefer the **open / arrival-rate** model for any SLA gate; if you must use closed, use a tool that corrects for coordinated omission.
- **Non-parity env.** Numbers from a laptop, a shared dev box, empty tables, or a cold cache describe that environment, not production. A test against a non-parity env is not a smaller version of the real result — it can be qualitatively wrong. Record parity in the run config or discard the result.
- **The client is the bottleneck.** If the **load generator** saturates first (its CPU pegged, its network NIC full, too few source ports, one underpowered box), you are measuring the generator, not the system — throughput plateaus but server CPU is idle. Check generator-side saturation every run; distribute the load across machines / use cloud generation when one box can't push the target rate.
- **One run ≠ signal.** A single campaign carries variance (noisy-neighbour VMs, cache warmth, background jobs). Repeat the run; report the median and the spread. A breakpoint seen once, unrepeated, is an anecdote — mirror `performance-principles`' variance discipline.
- **Averages lie.** A mean latency under budget can hide a p99 that blows it; always gate on the tail (p95/p99), never the mean.

## Halt conditions

- **Refuse a capacity claim with no run config + parity note.** "Handles the load" without tool, model, stages, rate, env, and the measured percentile is not a result — halt and demand the run.
- **Single-VU / quick-load numbers are not capacity.** A number from one virtual user, a hammer loop, or a non-parity box may not be presented as system capacity — halt and re-run the campaign under a realistic mix on a parity env.
- **No SLA gate defined before the run** — halt; define `p99 < X at Y rps with < Z% errors` first, or the run has no verdict to reach.
- **Closed model quoted as an SLA pass** — halt; re-run open / arrival-rate before certifying, or the pass may be coordinated-omission theatre.
- **Hand the ROOT CAUSE off, don't fix it here.** This skill owns the campaign and the verdict — it finds the knee and names the saturated resource. It does NOT fix the bottleneck: route the fix to `@performance-optimizer` / `@caching-architect` / the relevant pack, and route single-subject diagnosis to `skill:profile-endpoint`. A load-test PR that also rewrites the query has exceeded its mandate.

## Related

- `profile-endpoint.md` — boundary: it diagnoses ONE slow subject under quick load; this skill runs the whole-system campaign. Hand it the subject at the knee for the flamegraph + slow-query root cause.
- `@performance-optimizer` — owner of the fix once the binding resource is named (pool sizing, query time, index).
- `@caching-architect` — read-offload / cache-strategy fixes when the knee binds on read load.
- `@capacity-planner` *(distributed-systems pack, when co-installed)* — analytical complement: predicts the breakpoint on paper (Little's Law, peak QPS × payload) that this campaign empirically confirms or refutes. Run the planner first, then validate here. Absent that pack there is no ledger to reconcile the knee against: report the measured breakpoint alone and label the headroom `measured, unmodelled` rather than implying a prediction agreed with it.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.