Load Testing¶
Test your application under realistic and peak load conditions.
When to Load Test¶
- Before major releases
- After significant changes
- Capacity planning
- Finding breaking points
Tools Overview¶
| Tool | Language | Strengths |
|---|---|---|
| Locust | Python | Easy scripting, distributed |
| k6 | JavaScript | Modern, good metrics |
| wrk | N/A | Simple, fast |
| Artillery | YAML/JS | Good for APIs |
Locust¶
Installation¶
Basic Test¶
# locustfile.py
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 3) # Wait 1-3 seconds between tasks
@task(3) # Weight: 3x more likely than weight 1
def view_homepage(self):
self.client.get("/")
@task(1)
def view_profile(self):
self.client.get("/profile")
def on_start(self):
"""Called when user starts."""
self.client.post("/login", json={
"username": "testuser",
"password": "password"
})
Running¶
# Web UI
locust -f locustfile.py --host=http://localhost:8000
# Headless
locust -f locustfile.py --host=http://localhost:8000 \
--headless -u 100 -r 10 -t 60s
# -u: Total users
# -r: Users spawned per second
# -t: Test duration
Advanced Scenarios¶
from locust import HttpUser, task, between, SequentialTaskSet
class UserBehavior(SequentialTaskSet):
"""Tasks run in order."""
@task
def browse_products(self):
self.client.get("/products")
@task
def view_product(self):
self.client.get("/products/1")
@task
def add_to_cart(self):
self.client.post("/cart", json={"product_id": 1})
@task
def checkout(self):
self.client.post("/checkout")
class EcommerceUser(HttpUser):
tasks = [UserBehavior]
wait_time = between(1, 5)
Distributed Testing¶
# Master
locust -f locustfile.py --master
# Workers (on multiple machines)
locust -f locustfile.py --worker --master-host=192.168.1.1
k6¶
Installation¶
Basic Test¶
// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // Virtual users
duration: '30s',
};
export default function() {
const res = http.get('http://localhost:8000/');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Running¶
Ramping Load¶
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users
{ duration: '1m', target: 20 }, // Stay at 20
{ duration: '30s', target: 50 }, // Ramp up to 50
{ duration: '1m', target: 50 }, // Stay at 50
{ duration: '30s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // Error rate under 1%
},
};
API Testing¶
import http from 'k6/http';
import { check } from 'k6';
const BASE_URL = 'http://localhost:8000';
export function setup() {
// Login and get token
const res = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: 'test@example.com',
password: 'password',
}), {
headers: { 'Content-Type': 'application/json' },
});
return { token: res.json('access_token') };
}
export default function(data) {
const headers = {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json',
};
// GET
const users = http.get(`${BASE_URL}/users`, { headers });
check(users, { 'get users ok': (r) => r.status === 200 });
// POST
const newUser = http.post(`${BASE_URL}/users`, JSON.stringify({
name: 'Test User',
email: `test-${Date.now()}@example.com`,
}), { headers });
check(newUser, { 'create user ok': (r) => r.status === 201 });
}
Interpreting Results¶
Key Metrics¶
| Metric | Good | Warning | Bad |
|---|---|---|---|
| p50 latency | < 100ms | < 500ms | > 500ms |
| p95 latency | < 500ms | < 1s | > 1s |
| p99 latency | < 1s | < 2s | > 2s |
| Error rate | < 0.1% | < 1% | > 1% |
| Throughput | Meets target | - | Below target |
Reading Percentiles¶
Response times:
p50: 45ms ← 50% of requests under 45ms
p90: 120ms ← 90% of requests under 120ms
p95: 250ms ← 95% of requests under 250ms
p99: 800ms ← 99% of requests under 800ms (watch this!)
Common Patterns¶
Healthy System:
Saturation:
Breaking Point:
Users: ▁▂▃▄░░░░ ← Can't add more users
Latency: ▇▇▇▇▇▇▇▇ ← Constantly high
Errors: ▅▇█▓▓▓▓▓ ← High error rate
Test Types¶
Smoke Test¶
Quick sanity check.
Load Test¶
Normal expected load.
export const options = {
stages: [
{ duration: '5m', target: 100 }, // Ramp up
{ duration: '10m', target: 100 }, // Steady
{ duration: '5m', target: 0 }, // Ramp down
],
};
Stress Test¶
Find the breaking point.
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '2m', target: 300 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 500 }, // Keep going until failure
],
};
Soak Test¶
Find memory leaks, connection issues over time.
export const options = {
stages: [
{ duration: '5m', target: 100 },
{ duration: '4h', target: 100 }, // Long duration
{ duration: '5m', target: 0 },
],
};
Best Practices¶
- Test realistic scenarios — Not just GET /
- Use production-like data — Size and variety matter
- Test from different locations — If users are global
- Monitor during tests — CPU, memory, DB connections
- Establish baselines — Know what's normal
- Automate in CI — Prevent regressions
- Document results — Track over time
CI Integration¶
# .github/workflows/load-test.yml
name: Load Test
on:
schedule:
- cron: '0 2 * * *' # Nightly
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6 load test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/load/script.js
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results/