Stage 4 · Provision
Scalability & Capacity
Load Testing Methods
Open-loop and closed-loop tests with k6, Locust, wrk, and realistic traffic mixes.
Why Load Test?
Load testing validates that your system handles expected traffic without degrading performance. It reveals bottlenecks, sizing issues, and configuration problems before they cause production outages. Every service should be load tested before launch.
Open-Loop vs Closed-Loop
| Model | How It Works | Use Case |
|---|---|---|
| Open-loop | Generates N requests/sec regardless of response time | Capacity testing, saturation testing |
| Closed-loop | Waits for response before sending next request | User simulation, throughput testing |
Open-loop testing reveals what happens when the system is overwhelmed. Closed-loop testing simulates realistic user behavior. Use both: open-loop for capacity limits, closed-loop for user experience.
k6 Load Testing
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 500 }, // Ramp up to 500 users
{ duration: '5m', target: 500 }, // Stay at 500 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
},
};
export default function () {
const res = http.get('https://api.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}k6 provides realistic ramp-up patterns, threshold-based pass/fail criteria, and detailed metrics. The test ramps from 100 to 500 users over 16 minutes.
Locust
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 default
def view_products(self):
self.client.get("/api/products")
@task(1) # Weight: 1x
def view_product_detail(self):
product_id = random.randint(1, 10000)
self.client.get(f"/api/products/{product_id}")
@task(1)
def search_products(self):
self.client.get("/api/products?q=laptop&sort=price")Locust simulates real users with weighted tasks. The view_products task runs 3x more often than search, reflecting realistic usage patterns.
Realistic Traffic Mixes
Traffic distribution (measured from production):
GET /products 40% (browse catalog)
GET /products/:id 25% (view product)
POST /cart 10% (add to cart)
GET /cart 8% (view cart)
POST /orders 5% (checkout)
GET /orders 7% (view order history)
GET /search 5% (search)
Load test should match this distribution.
Do NOT just test GET /products — test the full mix.A realistic traffic mix tests the entire system, not just the most common endpoint. Checkout involves payment, inventory, and order services — it is the most critical path.
Load Test Checklist
- Realistic traffic mix — Match production request distribution.
- Ramp-up pattern — Gradually increase load, not instant spike.
- Think time — Add realistic pauses between requests.
- Test data — Use production-like data volumes and distributions.
- Warm cache — Test with warm caches and cold caches separately.
- External dependencies — Mock or point to staging dependencies.
- Metrics collection — Monitor during the test to identify bottlenecks.
Load testing production can cause outages for real users. Use a staging environment that mirrors production. If you must load test production, do it during a maintenance window with approval.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.