4 Load Test Shapes
Load, stress, spike, and soak โ each answers a different question about your system. See how the same shape looks in three popular tools (K6, Artillery, Locust), with ready-to-run snippets targeting this Practice API.
1. Load test โ baseline under expected traffic
Intent: Hold steady concurrency at your documented peak for 5โ10 minutes. Confirm the system meets its SLO under normal load.
When to use: Run on every release candidate. If it regresses by >10 %, block promotion.
SLO to assert: p95 latency < 300 ms, error rate < 0.1 %, throughput โฅ target RPS
// k6 โ hold 50 virtual users for 10 minutes
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp
{ duration: '10m', target: 50 }, // hold
{ duration: '1m', target: 0 }, // ramp-down
],
thresholds: {
http_req_duration: ['p(95)<300'],
http_req_failed: ['rate<0.001'],
},
};
export default function () {
const res = http.get('https://api.qa-practice.dev/api/v1/users');
check(res, { '200': (r) => r.status === 200 });
}# artillery โ equivalent shape
config:
target: https://api.qa-practice.dev
phases:
- duration: 120
arrivalRate: 5
rampTo: 50
name: ramp
- duration: 600
arrivalRate: 50
name: steady
ensure:
p95: 300
maxErrorRate: 0.1
scenarios:
- flow:
- get:
url: /api/v1/users# locust โ equivalent. Run: locust -f baseline.py
from locust import HttpUser, task, between
class QAUser(HttpUser):
wait_time = between(0.1, 0.3)
@task
def list_users(self):
with self.client.get('/api/v1/users', catch_response=True) as r:
if r.elapsed.total_seconds() > 0.3:
r.failure('p95 budget exceeded')
# Then: locust --users 50 --spawn-rate 5 --run-time 10m --headless2. Stress test โ find the breaking point
Intent: Ramp concurrency beyond expected peak until errors climb or latency degrades past threshold. Capture the ceiling.
When to use: Capacity planning, sizing autoscaling thresholds, understanding failure modes.
SLO to assert: Report at which load level p95 doubles OR error rate exceeds 1 %
// k6 โ step load until pain
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 800 },
{ duration: '2m', target: 0 },
],
// NO thresholds โ we want to see where it breaks, not gate on green
};
export default function () {
http.get('https://api.qa-practice.dev/api/v1/rate-limited');
}config:
target: https://api.qa-practice.dev
phases:
- { duration: 120, arrivalRate: 100, name: step-100 }
- { duration: 120, arrivalRate: 200, name: step-200 }
- { duration: 120, arrivalRate: 400, name: step-400 }
- { duration: 120, arrivalRate: 800, name: step-800 }
scenarios:
- flow:
- get: { url: /api/v1/rate-limited }class Stress(HttpUser):
wait_time = between(0.01, 0.05)
@task
def hit(self):
self.client.get('/api/v1/rate-limited')
# locust --users 800 --spawn-rate 100 --run-time 10m --headless
# Watch the chart โ note the knee in p95 and error-rate curves3. Spike test โ sudden surge
Intent: Jump from idle to 10ร peak in seconds. Mirrors real-world events: viral tweet, flash sale, outage recovery.
When to use: Validate autoscaling responsiveness, queue backpressure, circuit-breaker fallback.
SLO to assert: No cascading failures; system stabilises within 2 minutes post-spike; no queue backlog persists after 5 minutes.
// k6 โ 2 min of quiet, 30s spike, 5 min recovery
export const options = {
stages: [
{ duration: '2m', target: 10 },
{ duration: '30s', target: 500 }, // spike!
{ duration: '5m', target: 10 }, // recovery
],
};
export default function () {
http.post('https://api.qa-practice.dev/api/v1/batch', JSON.stringify({
operations: [{ method: 'GET', path: '/users' }],
}));
}config:
target: https://api.qa-practice.dev
phases:
- { duration: 120, arrivalRate: 10 }
- { duration: 30, arrivalRate: 500 } # spike
- { duration: 300, arrivalRate: 10 } # recovery
scenarios:
- flow:
- post:
url: /api/v1/batch
json:
operations: [{ method: GET, path: /users }]# Shape classes let you script arbitrary curves
from locust import LoadTestShape
class Spike(LoadTestShape):
stages = [
{ 'duration': 120, 'users': 10, 'spawn_rate': 5 },
{ 'duration': 150, 'users': 500, 'spawn_rate': 500 },
{ 'duration': 450, 'users': 10, 'spawn_rate': 5 },
]
def tick(self):
t = self.get_run_time()
for s in self.stages:
if t < s['duration']:
return (s['users'], s['spawn_rate'])
return None4. Soak / endurance test โ slow leaks
Intent: Hold moderate load for hours or days. Surfaces memory leaks, connection-pool exhaustion, slow queries, log-disk fill.
When to use: Pre-release for long-running services. Catches bugs invisible in 10-minute load tests.
SLO to assert: Memory RSS flat over 8h; connection count flat; no unbounded log growth; no performance drift
export const options = {
stages: [
{ duration: '5m', target: 30 },
{ duration: '8h', target: 30 }, // hold 8 hours
{ duration: '5m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'], // allow some drift budget
},
};
export default function () {
http.get('https://api.qa-practice.dev/api/v1/users');
}config:
target: https://api.qa-practice.dev
phases:
- { duration: 300, arrivalRate: 30 }
- { duration: 28800, arrivalRate: 30 } # 8h soak
scenarios:
- flow: [{ get: { url: /api/v1/users } }]# Run with master/worker for long soaks:
# master: locust --master --headless --users 30 --spawn-rate 5 --run-time 8h
# worker: locust --worker --master-host=<ip>
#
# Collect JVM/Node RSS + DB connection count via Prometheus alongside the run