Skip to content

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

pip install locust

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

# macOS
brew install k6

# Docker
docker run -i grafana/k6 run - <script.js

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

k6 run script.js

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:

Users: ▁▂▃▄▅▆▇█
Latency: ▁▁▁▁▁▁▁▁
Errors: ▁▁▁▁▁▁▁▁

Saturation:

Users: ▁▂▃▄▅▆▇█
Latency: ▁▁▂▃▅▇█▓  ← Increases with load
Errors: ▁▁▁▁▂▃▅▇  ← Errors start appearing

Breaking Point:

Users: ▁▂▃▄░░░░  ← Can't add more users
Latency: ▇▇▇▇▇▇▇▇  ← Constantly high
Errors: ▅▇█▓▓▓▓▓  ← High error rate

Test Types

Smoke Test

Quick sanity check.

export const options = {
  vus: 1,
  duration: '1m',
};

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

  1. Test realistic scenarios — Not just GET /
  2. Use production-like data — Size and variety matter
  3. Test from different locations — If users are global
  4. Monitor during tests — CPU, memory, DB connections
  5. Establish baselines — Know what's normal
  6. Automate in CI — Prevent regressions
  7. 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/