Skip to main content
Back to Practice API
Performance Testing Lab

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
// 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
# 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
# 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 --headless

2. 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
// 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');
}
Artillery
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 }
Locust
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 curves

3. 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
// 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' }],
  }));
}
Artillery
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 }]
Locust
# 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 None

4. 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

K6
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');
}
Artillery
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 } }]
Locust
# 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

Related Labs