Stage 6 · Operate
Capacity & Performance Engineering
Load Testing Strategy
k6, Locust, realistic traffic mixes, ramp profiles, and failure criteria — validating capacity before users suffer.
Why Load Test?
Load testing validates that your system can handle expected traffic before users experience problems. It reveals bottlenecks, capacity limits, and failure modes that only appear under load. Do not wait for production to find out your system cannot handle traffic.
The best time to load test is before a major launch or traffic event. The second best time is now. Load testing in production during a traffic spike is too late.
Load Testing Tools
| Tool | Language | Best For | Tradeoffs |
|---|---|---|---|
| k6 | JavaScript | HTTP APIs, microservices | Limited protocol support |
| Locust | Python | Complex scenarios, custom protocols | Slower than k6 |
| Vegeta | Go | Simple HTTP load testing | Limited scripting |
| JMeter | Java | Enterprise, complex protocols | Heavy, slow |
Realistic Traffic Mixes
Do not test with uniform requests. Real traffic is a mix of reads, writes, searches, and background jobs. Model your load test after actual production traffic patterns for meaningful results.
traffic_mix:
read_heavy_api:
- endpoint: "/api/users"
method: "GET"
weight: 60 # 60% of requests
payload_size: "small"
- endpoint: "/api/users"
method: "POST"
weight: 10 # 10% of requests
payload_size: "medium"
- endpoint: "/api/search"
method: "GET"
weight: 25 # 25% of requests
payload_size: "variable"
- endpoint: "/api/analytics"
method: "GET"
weight: 5 # 5% of requests
payload_size: "large"
write_heavy_api:
- endpoint: "/api/events"
method: "POST"
weight: 70
payload_size: "medium"
- endpoint: "/api/events"
method: "GET"
weight: 20
payload_size: "small"
- endpoint: "/api/reports"
method: "GET"
weight: 10
payload_size: "large"Ramp Profiles
Ramp profiles gradually increase load to find the breaking point. A sudden spike may overwhelm the system before autoscaling kicks in. Gradual ramps reveal where performance degrades and where it breaks.
ramp_profiles:
ramp_to_breaking:
description: "Find the maximum capacity"
stages:
- duration: "2m"
target: 100 # Ramp up to 100 VUs
- duration: "5m"
target: 100 # Hold at 100 VUs
- duration: "2m"
target: 200 # Ramp to 200 VUs
- duration: "5m"
target: 200 # Hold at 200 VUs
- duration: "2m"
target: 500 # Ramp to 500 VUs
- duration: "5m"
target: 500 # Hold at 500 VUs
- duration: "5m"
target: 0 # Ramp down
soak_test:
description: "Validate stability under sustained load"
stages:
- duration: "5m"
target: 200
- duration: "4h"
target: 200 # Hold for 4 hours
- duration: "5m"
target: 0
spike_test:
description: "Simulate sudden traffic spike"
stages:
- duration: "1m"
target: 100
- duration: "30s"
target: 1000 # Spike to 1000 VUs
- duration: "5m"
target: 1000 # Hold spike
- duration: "30s"
target: 100 # Recovery
- duration: "5m"
target: 100
- duration: "1m"
target: 0Failure Criteria
Define pass/fail criteria before running the test. Without criteria, you are just generating traffic without learning anything. The criteria should align with your SLOs.
failure_criteria:
latency:
p95: "< 500ms"
p99: "< 1000ms"
p99_9: "< 2000ms"
error_rate:
threshold: "< 1%"
measurement: "Percentage of non-2xx responses"
throughput:
minimum_rps: 1000
measurement: "Requests per second sustained"
saturation:
cpu_max: "< 80%"
memory_max: "< 85%"
connection_pool_usage: "< 90%"
recovery:
max_recovery_time: "5 minutes"
description: "Time to return to baseline after load stops"
abort_criteria:
- "Error rate > 10% for 1 minute"
- "Latency p99 > 5s for 1 minute"
- "Service returns 503 for any request"
- "Data corruption detected"k6 Examples
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const latencyP99 = new Trend('latency_p99');
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(99)<1000'],
errors: ['rate<0.01'],
},
};
export default function () {
const weight = Math.random();
let response;
if (weight < 0.6) {
// 60% read requests
response = http.get('https://api.example.com/users');
} else if (weight < 0.7) {
// 10% write requests
response = http.post('https://api.example.com/users',
JSON.stringify({ name: 'Test User' }),
{ headers: { 'Content-Type': 'application/json' } }
);
} else {
// 30% search requests
response = http.get('https://api.example.com/search?q=test');
}
check(response, {
'status is 2xx': (r) => r.status >= 200 && r.status < 300,
'latency < 1s': (r) => r.timings.duration < 1000,
});
errorRate.add(response.status >= 500);
latencyP99.add(response.timings.duration);
sleep(0.1);
}Run full load tests in staging to find major bottlenecks. Run lighter load tests in production to validate with real infrastructure and traffic patterns. Never run production load tests without approval and safety controls.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.