Load and performance testing
Turn a performance question into a workload model, an executable test with explicit pass/fail thresholds, and an interpretation that separates measured facts from inference.
When to invoke
- "Load test this API before launch."
- "Find the breaking point of our checkout service."
- "How many users can this handle?"
- "Set up a soak test to catch memory leaks."
- "Our p99 latency looks bad, help me measure it properly."
Choose the test type first
The question determines the profile. Running the wrong profile produces confident but meaningless numbers.
| Question |
Profile |
Shape |
| Does it meet the SLO at expected traffic? |
Load |
Ramp to target, hold, ramp down |
| Where does it break? |
Stress |
Step up until failure, record the step |
| Does it degrade over hours? |
Soak |
Hold moderate load for hours |
| Can it survive a sudden surge? |
Spike |
Jump to peak instantly, observe recovery |
| What does one user cost? |
Baseline |
Single user, no contention |
Always run a baseline first. Without it you cannot separate contention from inherently slow code.
Tool selection
| Tool |
Language |
Best fit |
Trade-off |
| k6 |
JavaScript |
Scriptable CI-first testing, good thresholds model |
No native browser workload; separate module needed |
| JMeter |
GUI plus XML |
Protocol breadth, teams wanting a GUI |
Verbose plans; heavier per-VU cost on one node |
| Locust |
Python |
Complex conditional user behavior |
Requires Python performance care in hot paths |
| Gatling |
Scala or Java DSL |
High throughput per node, expressive DSL |
Steeper language ramp for non-JVM teams |
Pick the tool the team can maintain. A precise test nobody can edit is worse than a rough test they own.
Threshold-driven tests
A test without thresholds cannot fail, so it cannot protect anything. Encode the SLO in the test.
// k6: fail the run when the SLO is violated
export const options = {
stages: [
{ duration: '2m', target: 100 }, // ramp
{ duration: '5m', target: 100 }, // hold
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
http_req_failed: ['rate<0.01'], // under 1% errors
http_req_duration: ['p(95)<500', 'p(99)<1500'],
checks: ['rate>0.99'],
},
};
Set thresholds from the agreed SLO, not from the first result. Deriving the threshold from observed output only re-states current behavior.
Workload modeling
Unrealistic load produces unusable data.
- Think time. Real users pause. Zero sleep turns 100 virtual users into a throughput far above 100 real users.
- Data variety. Reusing one ID makes every cache hit. Parameterize from a dataset.
- Traffic mix. Weight endpoints by production ratios, not by equal split.
- Ramp, do not slam. Except in a deliberate spike test, ramp so autoscaling and connection pools behave as in production.
- Cold start. Decide explicitly whether warm-up is in scope, then state it.
Reading results honestly
- Report percentiles, not averages. An average hides the tail that users feel. Report p50, p95, p99, and max.
- Check the error rate before the latency. Fast responses that are all 500s look excellent on a latency chart.
- Confirm the load generator was not the bottleneck. Saturated CPU or exhausted ports on the client invalidate the run.
- One variable per run. Changing load and configuration together makes attribution impossible.
- Correlate with server-side telemetry. Client latency alone cannot distinguish network, queue, and compute time.
Gotchas
- The client can be the bottleneck. Always record generator CPU, memory, and socket usage alongside results.
- Shared environments invalidate comparisons. Another tenant's load becomes your noise; state the environment and its isolation.
- Connection reuse changes everything. Keep-alive on or off can shift results by an order of magnitude.
- DNS and TLS handshakes may dominate short tests. Warm the pool or measure them deliberately.
- Never load test production without written approval, a blast-radius limit, and an abort trigger.
Output template
## Load test result
**Status:** met-slo | violated-slo | inconclusive
**Summary:** <profile, target load, and the headline outcome>
### Details
| Metric | Result | Threshold |
| --- | --- | --- |
| Requests/sec | <value> | <target> |
| p95 latency | <value> | <threshold> |
| p99 latency | <value> | <threshold> |
| Error rate | <value> | <threshold> |
Environment: <where it ran and how isolated>
Workload model: <think time, data variety, traffic mix>
### Validation
- Load generator saturation: <checked and result>
- Server-side telemetry correlation: <checked and result>
Quality gate
References
1---2name: load-performance-testing3description: Design and run load, stress, soak, and spike tests with k6, JMeter, Locust, or Gatling, define latency and error-rate thresholds, model realistic workloads, and interpret results without drawing unsupported conclusions. Use when the user asks to load test a service, find a breaking point, size capacity, set performance SLOs, tune a stress or soak profile, or explain latency percentiles.4license: MIT5---67<!-- Generated from harness/github-copilot/skills/load-performance-testing/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->89# Load and performance testing1011Turn a performance question into a workload model, an executable test with explicit pass/fail thresholds, and an interpretation that separates measured facts from inference.1213## When to invoke1415- "Load test this API before launch."16- "Find the breaking point of our checkout service."17- "How many users can this handle?"18- "Set up a soak test to catch memory leaks."19- "Our p99 latency looks bad, help me measure it properly."2021## Choose the test type first2223The question determines the profile. Running the wrong profile produces confident but meaningless numbers.2425| Question | Profile | Shape |26| --- | --- | --- |27| Does it meet the SLO at expected traffic? | Load | Ramp to target, hold, ramp down |28| Where does it break? | Stress | Step up until failure, record the step |29| Does it degrade over hours? | Soak | Hold moderate load for hours |30| Can it survive a sudden surge? | Spike | Jump to peak instantly, observe recovery |31| What does one user cost? | Baseline | Single user, no contention |3233Always run a baseline first. Without it you cannot separate contention from inherently slow code.3435## Tool selection3637| Tool | Language | Best fit | Trade-off |38| --- | --- | --- | --- |39| k6 | JavaScript | Scriptable CI-first testing, good thresholds model | No native browser workload; separate module needed |40| JMeter | GUI plus XML | Protocol breadth, teams wanting a GUI | Verbose plans; heavier per-VU cost on one node |41| Locust | Python | Complex conditional user behavior | Requires Python performance care in hot paths |42| Gatling | Scala or Java DSL | High throughput per node, expressive DSL | Steeper language ramp for non-JVM teams |4344Pick the tool the team can maintain. A precise test nobody can edit is worse than a rough test they own.4546## Threshold-driven tests4748A test without thresholds cannot fail, so it cannot protect anything. Encode the SLO in the test.4950```javascript51// k6: fail the run when the SLO is violated52export const options = {53 stages: [54 { duration: '2m', target: 100 }, // ramp55 { duration: '5m', target: 100 }, // hold56 { duration: '2m', target: 0 }, // ramp down57 ],58 thresholds: {59 http_req_failed: ['rate<0.01'], // under 1% errors60 http_req_duration: ['p(95)<500', 'p(99)<1500'],61 checks: ['rate>0.99'],62 },63};64```6566Set thresholds from the agreed SLO, not from the first result. Deriving the threshold from observed output only re-states current behavior.6768## Workload modeling6970Unrealistic load produces unusable data.7172- **Think time.** Real users pause. Zero sleep turns 100 virtual users into a throughput far above 100 real users.73- **Data variety.** Reusing one ID makes every cache hit. Parameterize from a dataset.74- **Traffic mix.** Weight endpoints by production ratios, not by equal split.75- **Ramp, do not slam.** Except in a deliberate spike test, ramp so autoscaling and connection pools behave as in production.76- **Cold start.** Decide explicitly whether warm-up is in scope, then state it.7778## Reading results honestly7980- **Report percentiles, not averages.** An average hides the tail that users feel. Report p50, p95, p99, and max.81- **Check the error rate before the latency.** Fast responses that are all 500s look excellent on a latency chart.82- **Confirm the load generator was not the bottleneck.** Saturated CPU or exhausted ports on the client invalidate the run.83- **One variable per run.** Changing load and configuration together makes attribution impossible.84- **Correlate with server-side telemetry.** Client latency alone cannot distinguish network, queue, and compute time.8586## Gotchas8788- **The client can be the bottleneck.** Always record generator CPU, memory, and socket usage alongside results.89- **Shared environments invalidate comparisons.** Another tenant's load becomes your noise; state the environment and its isolation.90- **Connection reuse changes everything.** Keep-alive on or off can shift results by an order of magnitude.91- **DNS and TLS handshakes may dominate short tests.** Warm the pool or measure them deliberately.92- **Never load test production without written approval**, a blast-radius limit, and an abort trigger.9394## Output template9596```markdown97## Load test result9899**Status:** met-slo | violated-slo | inconclusive100**Summary:** <profile, target load, and the headline outcome>101102### Details103| Metric | Result | Threshold |104| --- | --- | --- |105| Requests/sec | <value> | <target> |106| p95 latency | <value> | <threshold> |107| p99 latency | <value> | <threshold> |108| Error rate | <value> | <threshold> |109110Environment: <where it ran and how isolated>111Workload model: <think time, data variety, traffic mix>112113### Validation114- Load generator saturation: <checked and result>115- Server-side telemetry correlation: <checked and result>116```117118## Quality gate119120- [ ] The test profile matches the question being asked.121- [ ] Thresholds come from an agreed SLO, not from observed output.122- [ ] A baseline single-user run exists for comparison.123- [ ] The workload model states think time, data variety, and traffic mix.124- [ ] Error rate was verified before latency was interpreted.125- [ ] Load generator saturation was ruled out.126- [ ] Environment and isolation are stated; unmeasured factors are called assumptions.127- [ ] Production testing, if any, had explicit approval and an abort trigger.128129## References130131- [k6 documentation](https://grafana.com/docs/k6/latest/)132- [Apache JMeter user manual](https://jmeter.apache.org/usermanual/index.html)133- [Locust documentation](https://docs.locust.io/en/stable/)134- [Gatling documentation](https://docs.gatling.io/)