Load, Stress and Soak Testing
Finding where a system breaks rather than how fast it is — load profiles, the difference between the four kinds of test, and reading the results honestly.
3 min read · updated 19 September 2026
Performance testing asks how fast the system is. Load testing asks what happens when you push it — and specifically, what breaks first.
That answer is more useful than a latency number, because it tells you what to fix and what to buy.
#Four tests, four questions
| Profile | Question | |
|---|---|---|
| Load | expected peak, sustained | can we serve the traffic we expect? |
| Stress | increase until failure | what breaks first, and how? |
| Spike | sudden 10x, then drop | does autoscaling and queueing cope? |
| Soak | moderate, for hours | does anything degrade over time? |
Most teams run the first and call it done. The second and fourth find the incidents.
#Load profiles
// k6 — four scenarios in one file, run one at a time by name.
export const options = {
scenarios: {
// 1. Load: the traffic we expect, held steady.
load: {
executor: 'constant-arrival-rate',
rate: 200, timeUnit: '1s', duration: '15m',
preAllocatedVUs: 200, maxVUs: 1000
},
// 2. Stress: climb until something gives.
stress: {
executor: 'ramping-arrival-rate',
startRate: 50, timeUnit: '1s',
preAllocatedVUs: 200, maxVUs: 3000,
stages: [
{ target: 200, duration: '5m' },
{ target: 500, duration: '5m' },
{ target: 1000, duration: '5m' },
{ target: 2000, duration: '5m' }
]
},
// 3. Spike: what a marketing email looks like.
spike: {
executor: 'ramping-arrival-rate',
startRate: 50, timeUnit: '1s',
preAllocatedVUs: 100, maxVUs: 3000,
stages: [
{ target: 50, duration: '2m' },
{ target: 1500, duration: '30s' }, // the email goes out
{ target: 1500, duration: '3m' },
{ target: 50, duration: '2m' } // does it recover?
]
},
// 4. Soak: modest load, long enough for slow problems to appear.
soak: {
executor: 'constant-arrival-rate',
rate: 100, timeUnit: '1s', duration: '8h',
preAllocatedVUs: 150, maxVUs: 400
}
}
};k6 run --env SCENARIO=stress script.js#Coordinated omission
The most important measurement error in load testing, and the reason many homegrown tests are optimistic by a factor of ten.
With a fixed pool of virtual users, each waits for its response before sending the next request. When the system slows down, the test slows down with it — so the offered load falls exactly when it should be rising, and the recorded latencies exclude all the requests that would have been sent during the slow period.
// Wrong: 100 VUs in a loop. Offered load drops as the system degrades.
export const options = { vus: 100, duration: '10m' };
// Right: requests are issued at a fixed rate regardless of response time.
export const options = {
scenarios: { steady: {
executor: 'constant-arrival-rate',
rate: 200, timeUnit: '1s', duration: '10m',
preAllocatedVUs: 200, maxVUs: 2000
}}
};If the runtime reports that it could not allocate enough VUs to maintain the rate, that is not a configuration warning — it is the finding. The system could not keep up.
#What to watch while it runs
The client-side numbers tell you that it broke. The server-side ones tell you why.
- Saturation: CPU, memory, disk I/O
- Connection pools: the most common bottleneck in web applications, and the one that produces the sharpest cliff
- Database: active connections, lock waits, slow query log, replication lag
- Queue depth, if anything is queued
- GC pauses, on a managed runtime
- Errors by type — timeouts, 5xx, connection refused — which usually identify the failing component directly
The shape of the failure is the diagnosis:
Latency climbs linearly, errors flat → a saturated resource, gracefully queueing
Latency flat, then a vertical cliff → a pool or a limit was exhausted
Errors spike, latency stays low → something is failing fast (circuit breaker, or a crash)
Latency climbs over hours at constant load → a leak. Soak test found it.#Soak testing
The one most often skipped, and the one that finds the problems that wake people up:
- memory leaks, which need hours to become visible
- connection pool exhaustion from a path that leaks one connection per thousand requests
- unbounded caches
- log files filling a disk
- token or session expiry not handled on a long-lived client
- a scheduled job that interacts badly with sustained load
Run it for eight hours against a production-like environment, overnight, weekly. Plot memory and connection counts over the whole window — the shape of the line is the result, not the final number.
#Test data volume
A load test against an empty database measures the wrong system. The query that is 4ms against 1,000 rows and 4 seconds against 10 million is the single most common production performance incident, and no amount of concurrency against a small dataset will reveal it.
Generate production-like volume and distribution — never copy production data, for the reasons in test data management — and include the skew: the one customer with 50,000 orders is the one whose page times out.
#Environments
Load testing needs an environment shaped like production. A test against a single container on a laptop tells you about the laptop.
Terraform for test infrastructure covers provisioning one on demand and destroying it afterwards, which is the only way this stays affordable.
#In production, carefully
Increasingly common, and reasonable with guard rails:
- Shadow traffic. Mirror real requests to a new version; discard the responses. Real load, real distribution, no user impact.
- A small synthetic percentage, clearly marked, with isolated data, and an automatic stop when error rates rise.
- Progressive rollout with real traffic — a canary — which is load testing with the safest possible profile.
A full stress test in production, without a stop condition, is how you create the incident you were trying to prevent.
Common questions
- What is the difference between load, stress, spike and soak testing?
- Load testing runs the traffic you expect and checks it is handled. Stress testing keeps increasing until something breaks, to find out what breaks first. Spike testing applies a sudden jump, to test autoscaling and queueing. Soak testing runs a moderate load for hours, to find leaks and slow degradation.
- How long should a soak test run?
- Long enough for the slow problems to appear — usually four to twenty-four hours. Memory leaks, connection pool exhaustion, log disk filling and cache unbounded growth all take hours to surface and none of them appear in a ten-minute run.
- Can I load test in production?
- With care, and a plan. Traffic shadowing and a small percentage of synthetic load are common and safe when data is isolated and marked. A full stress test in production is how you cause the incident you were trying to prevent.
Runnable samples for this page
last test results ↗- JavaScript (k6)
k6/quality/load-testing
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Performance TestingLatency, throughput and what breaks first — designing a performance test that answers a question, reading percentiles honestly, and putting a check in the pipeline.
- Database TestingTesting migrations, constraints, queries and transactions — the layer everything else depends on, and the one most suites replace with a mock.
- Terraform for Test InfrastructureProvisioning ephemeral test environments as code — per-branch stacks, workspaces, cost controls, and testing the Terraform itself.
- Running Tests in ParallelHow parallelism turns hidden coupling into failures, the shared resources that clash, and how to make a suite genuinely safe to run concurrently.
- Playwright ShardingSplitting a browser suite across machines and merging the reports back — the mechanics, the prerequisites, and how to choose a shard count that is actually faster.