name: load-testing-patterns description: k6 script templates, load profiles, response time thresholds, SLO validation, and performance testing strategies.
Load Testing Patterns
Performance validation with k6 for SLO-driven load testing.
Basic k6 Script Template
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate, Trend } from 'k6/metrics'
// Custom metrics
const errorRate = new Rate('errors')
const loginDuration = new Trend('login_duration')
// Thresholds define pass/fail criteria
export const options = {
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th < 500ms, 99th < 1s
http_req_failed: ['rate<0.01'], // Error rate < 1%
errors: ['rate<0.05'], // Custom error rate < 5%
},
scenarios: {
smoke: {
executor: 'constant-vus',
vus: 5,
duration: '1m',
}
}
}
export default function () {
const res = http.get('https://api.example.com/health')
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body has status': (r) => JSON.parse(r.body).status === 'ok',
}) || errorRate.add(1)
sleep(1)
}
Load Profiles
export const options = {
scenarios: {
// 1. Smoke Test: verify system works under minimal load
smoke: {
executor: 'constant-vus',
vus: 3,
duration: '1m',
},
// 2. Load Test: normal expected traffic
load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 }, // Ramp up
{ duration: '5m', target: 50 }, // Steady state
{ duration: '2m', target: 0 }, // Ramp down
],
},
// 3. Stress Test: find breaking point
stress: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 }, // Beyond normal
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 }, // Breaking point?
{ duration: '5m', target: 300 },
{ duration: '5m', target: 0 },
],
},
// 4. Spike Test: sudden traffic surge
spike: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '30s', target: 10 },
{ duration: '10s', target: 500 }, // Instant spike
{ duration: '1m', target: 500 },
{ duration: '10s', target: 10 }, // Instant drop
{ duration: '1m', target: 10 },
],
},
// 5. Soak Test: sustained load over time (memory leaks, connection exhaustion)
soak: {
executor: 'constant-vus',
vus: 50,
duration: '2h',
},
}
}
Realistic User Scenarios
import http from 'k6/http'
import { check, group, sleep } from 'k6'
const BASE_URL = __ENV.BASE_URL || 'https://api.example.com'
export default function () {
// Simulate real user journey, not isolated endpoints
let token
group('01_login', () => {
const res = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: `user${__VU}@test.com`,
password: 'testpass123'
}), { headers: { 'Content-Type': 'application/json' } })
check(res, { 'login successful': (r) => r.status === 200 })
token = JSON.parse(res.body).token
})
sleep(Math.random() * 3 + 1) // Think time: 1-4 seconds
group('02_browse_products', () => {
const headers = { Authorization: `Bearer ${token}` }
const listRes = http.get(`${BASE_URL}/products?page=1&limit=20`, { headers })
check(listRes, { 'products loaded': (r) => r.status === 200 })
const products = JSON.parse(listRes.body).data
if (products.length > 0) {
const product = products[Math.floor(Math.random() * products.length)]
const detailRes = http.get(`${BASE_URL}/products/${product.id}`, { headers })
check(detailRes, { 'product detail loaded': (r) => r.status === 200 })
}
})
sleep(Math.random() * 2 + 1)
group('03_add_to_cart', () => {
const res = http.post(`${BASE_URL}/cart/items`, JSON.stringify({
productId: 'prod_001',
quantity: 1
}), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
check(res, { 'item added to cart': (r) => r.status === 201 })
})
}
SLO Validation
export const options = {
thresholds: {
// SLO: Availability > 99.9%
http_req_failed: ['rate<0.001'],
// SLO: p50 < 100ms, p95 < 500ms, p99 < 1000ms
http_req_duration: [
'p(50)<100',
'p(95)<500',
'p(99)<1000',
],
// SLO per endpoint using tags
'http_req_duration{name:login}': ['p(95)<800'],
'http_req_duration{name:get_products}': ['p(95)<200'],
'http_req_duration{name:checkout}': ['p(95)<2000'],
// SLO: Throughput > 1000 RPS
http_reqs: ['rate>1000'],
}
}
// Tag requests for per-endpoint SLOs
export default function () {
http.get(`${BASE_URL}/products`, { tags: { name: 'get_products' } })
http.post(`${BASE_URL}/auth/login`, payload, { tags: { name: 'login' } })
}
CI Integration
# .github/workflows/load-test.yml
name: Load Test
on:
pull_request:
branches: [main]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: grafana/k6-action@v0.3.1
with:
filename: tests/load/smoke.js
env:
BASE_URL: ${{ secrets.STAGING_URL }}
# k6 exits non-zero if thresholds fail → PR check fails
Checklist
- Run smoke test on every PR (fast, catches regressions)
- Load test weekly against staging with production-like data
- Stress test quarterly to find breaking points
- Soak test before major releases (2+ hours, detect memory leaks)
- Thresholds set per endpoint, not just global p95
- Realistic think times between requests (sleep 1-5s)
- Use multiple VU scenarios (not everyone does the same thing)
- Store results in Grafana/InfluxDB for trend analysis
Anti-Patterns
- Testing only happy paths: include error scenarios (404, 429, 500)
- No think time: unrealistic request rate, not how users behave
- Testing against production without traffic control (use staging)
- Single endpoint tests: real users hit multiple endpoints per session
- Ignoring connection time: p95 duration hides DNS/TLS overhead
- Hardcoded test data: VUs sharing same user account causes contention