"We load tested it" can mean four very different things. Each test answers a different question, and running the wrong one gives you a confident answer to a question you didn't ask.
| Test | Question | Shape | Length |
|---|---|---|---|
| Load | Do we meet our goals at expected traffic? | Ramp to peak, hold | 10–30 min |
| Stress | Where and how do we break? | Keep increasing | Until it fails |
| Spike | Do we survive a sudden burst? | Instant jump | 5–15 min |
| Soak | Do we stay healthy for hours? | Moderate, constant | 2–24 h |
All examples below use k6's ramping-arrival-rate executor, so the load is new users per second, which is how real traffic arrives. Only the stages change.
// shared setup
import http from 'k6/http';
export default function () {
http.get(`${__ENV.BASE_URL}/products`);
}
1. Load test: "are we ready?"
Simulate the traffic you expect and check it against goals.
export const options = {
scenarios: {
load: {
executor: 'ramping-arrival-rate',
startRate: 0, timeUnit: '1s', preAllocatedVUs: 200, maxVUs: 2000,
stages: [
{ target: 50, duration: '5m' }, // ramp to expected peak
{ target: 50, duration: '20m' }, // hold
{ target: 0, duration: '2m' },
],
},
},
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
Clear pass/fail: thresholds hold or they don't. Run it before every launch and on a schedule after.
2. Stress test: "where's the ceiling?"
Keep increasing past expected traffic until something gives.
stages: [
{ target: 50, duration: '3m' },
{ target: 100, duration: '3m' },
{ target: 200, duration: '3m' },
{ target: 400, duration: '3m' },
{ target: 0, duration: '3m' }, // watch the recovery!
],
You learn two things:
- The breaking point: the load where p95 or errors cross your limits.
-
The failure mode: graceful slowdown, clean
503s, or a cliff that takes minutes to recover from.
That last ramp-down stage matters: a system that doesn't recover on its own needs work, whatever its ceiling is.
3. Spike test: "what if we go viral?"
Real traffic doesn't ramp politely. Newsletters, Hacker News, a sale at 10:00 sharp.
stages: [
{ target: 10, duration: '2m' }, // normal
{ target: 300, duration: '10s' }, // spike
{ target: 300, duration: '5m' }, // hold
{ target: 10, duration: '1m' },
],
Spikes find what gentle ramps never do:
- autoscaling that needs 3 minutes while the spike takes 10 seconds
- cold caches sending everything to the database at once
- pools sized for the average
- one hot row (the stock count of the sale item) everyone writes to
4. Soak test: "can we run all week?"
Moderate load, held for hours.
stages: [
{ target: 30, duration: '5m' },
{ target: 30, duration: '4h' },
{ target: 0, duration: '5m' },
],
You're looking for drift: something that moves while the load doesn't.
- memory that climbs and never plateaus → leak or unbounded cache
- connections that slowly run out → resources not released on error paths
- latency creeping up hour by hour → growing tables, fragmentation, GC pressure
- errors at the same clock time → a cron job colliding with traffic
Compare the first hour's p95 with the last hour's. Healthy systems look boring.
Which one when?
- Every pull request: a short, steady load test that catches regressions
- Before every launch: load test at expected peak
- Quarterly or after big changes: stress test to know your ceiling
- Before known events: spike test shaped like the event
- Before trusting a new service for weeks: soak test, overnight
TL;DR
Same user journey, four load shapes, four different questions. If you only ever ramp to peak and hold, you've answered "are we ready for a normal Tuesday?" and nothing else.
I build Hoofbeat, where all four are the same flow with a different load shape: schedule the soak overnight and get a Slack alert only if it regresses. Original post on the Hoofbeat blog.
Top comments (0)