Most teams that do performance testing do exactly one kind: a fixed number of users for a fixed duration. That's a useful baseline, and it misses three failure modes that only appear under other conditions.
1. Load test: does it hold up under expected traffic?
The baseline. Ramp to your realistic peak and stay there.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up
{ duration: '10m', target: 100 }, // hold at expected peak
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500', 'p(99)<1500'],
},
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/api/items`);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
Two things about this that matter more than the numbers. Use percentiles, not averages: an average of 200ms can hide a p99 of nine seconds, and the p99 is somebody's actual experience. And put your requirements in thresholds, so the test passes or fails on its own instead of needing someone to squint at a graph.
2. Stress test: where does it break, and how?
Push past expected peak until something gives. The number you're looking for isn't the breaking point itself, it's the behaviour at the breaking point.
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 300 },
{ duration: '5m', target: 600 },
{ duration: '5m', target: 1000 },
{ duration: '5m', target: 0 },
],
};
Good behaviour under stress is degrading: slower responses, queueing, maybe a polite 503. Bad behaviour is a cascade, where one exhausted connection pool takes out three unrelated endpoints. You want to know which one you have before a marketing campaign tells you.
Also watch the ramp down. A system that recovers to normal latency within a minute is healthy. One that stays slow for twenty minutes has a queue it never drains.
3. Spike test: what happens when traffic arrives all at once?
Real traffic doesn't ramp politely over two minutes. A newsletter goes out, or a post gets popular, and load goes up tenfold in thirty seconds.
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '30s', target: 1000 }, // the spike
{ duration: '3m', target: 1000 },
{ duration: '30s', target: 50 },
{ duration: '3m', target: 50 }, // does it recover?
],
};
This is where autoscaling gets tested honestly. Scaling up takes time; the spike test tells you how much traffic gets dropped while you wait, and whether the cache stampedes when everything comes back cold.
4. Soak test: what leaks?
Same load as a normal load test, run for hours. Nothing dramatic happens for the first thirty minutes, which is exactly the point.
export const options = {
stages: [
{ duration: '5m', target: 100 },
{ duration: '4h', target: 100 },
{ duration: '5m', target: 0 },
],
};
Soak tests find memory leaks, connection pools that never release, log files filling a disk, and database tables that grow until a query without an index falls off a cliff. These are the problems that produce a mysterious restart every Thursday afternoon in production.
Practical notes
Test somewhere realistic. Running k6 from your laptop against a service behind a rate limiter measures your home internet connection and the rate limiter.
Correlate with server-side metrics. k6 tells you the response was slow. CPU, memory, connection counts and slow query logs tell you why. Without both halves you get a number and no action.
Start small and go up in steps. Watch where the latency curve bends. That inflection point is more informative than the point where it errors.
Run the load test in CI, run the rest on a schedule. A ten-minute load test on a nightly build catches regressions. Nobody merges a pull request that waits four hours for a soak test.