Load testing answers the question every production deployment raises: “What happens under real traffic?” Claude Code generates k6 scripts that simulate realistic user behavior, establishes performance baselines, and helps interpret results to identify exactly where bottlenecks are.
Basic Load Test with k6
Write a k6 load test for our checkout API.
Simulate: browse products → add to cart → checkout.
Run 100 concurrent users for 5 minutes.
Exit with non-zero if p99 response time > 500ms or error rate > 1%.
// tests/load/checkout-flow.js
import http from 'k6/http';
import { sleep, check, group } from 'k6';
import { Counter, Rate, Trend } from 'k6/metrics';
// Custom metrics
const checkoutDuration = new Trend('checkout_duration', true); // true = milliseconds
const cartErrors = new Counter('cart_errors');
const checkoutSuccessRate = new Rate('checkout_success_rate');
export const options = {
stages: [
{ duration: '1m', target: 20 }, // Ramp up to 20 users
{ duration: '3m', target: 100 }, // Ramp up to 100 users
{ duration: '1m', target: 100 }, // Hold at 100 users
{ duration: '30s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(99)<500'], // 99th percentile < 500ms
http_req_failed: ['rate<0.01'], // Error rate < 1%
checkout_duration: ['p(95)<1000'], // 95% of checkouts < 1s
checkout_success_rate: ['rate>0.99'], // 99% checkout success
},
};
const BASE_URL = __ENV.BASE_URL || 'https://api.staging.example.com';
// Test data — randomize to avoid cache artifacts
const products = ['prod-1', 'prod-2', 'prod-3', 'prod-4', 'prod-5'];
const testUsers = [
{ email: '[email protected]', password: 'test-password' },
{ email: '[email protected]', password: 'test-password' },
// ... more test users
];
export default function () {
const user = testUsers[Math.floor(Math.random() * testUsers.length)];
let authToken;
group('Authentication', () => {
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: user.email,
password: user.password,
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login successful': r => r.status === 200,
'has auth token': r => r.json('token') !== null,
});
authToken = loginRes.json('token');
});
if (!authToken) {
cartErrors.add(1);
return;
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
};
group('Browse products', () => {
const productId = products[Math.floor(Math.random() * products.length)];
const catalogRes = http.get(`${BASE_URL}/products?page=1&limit=20`, { headers });
check(catalogRes, { 'catalog loads': r => r.status === 200 });
sleep(1); // Think time
const productRes = http.get(`${BASE_URL}/products/${productId}`, { headers });
check(productRes, { 'product detail loads': r => r.status === 200 });
sleep(2);
});
let cartId;
group('Add to cart', () => {
const productId = products[Math.floor(Math.random() * products.length)];
const cartRes = http.post(`${BASE_URL}/cart`, JSON.stringify({
productId,
quantity: 1,
}), { headers });
const success = check(cartRes, {
'item added to cart': r => r.status === 200 || r.status === 201,
});
if (!success) {
cartErrors.add(1);
return;
}
cartId = cartRes.json('cartId');
sleep(1);
});
if (!cartId) return;
group('Checkout', () => {
const startTime = Date.now();
const checkoutRes = http.post(`${BASE_URL}/checkout`, JSON.stringify({
cartId,
paymentMethod: { type: 'test_card', token: 'tok_visa' },
shippingAddressId: 'addr-test-1',
}), { headers });
const duration = Date.now() - startTime;
checkoutDuration.add(duration);
const success = check(checkoutRes, {
'checkout successful': r => r.status === 200 || r.status === 201,
'has order id': r => r.json('orderId') !== null,
});
checkoutSuccessRate.add(success ? 1 : 0);
sleep(1);
});
}
// Lifecycle hooks for setup/teardown
export function setup() {
// Verify API is accessible before starting
const res = http.get(`${BASE_URL}/health`);
if (res.status !== 200) {
throw new Error(`Health check failed: ${res.status}`);
}
console.log('Health check passed. Starting load test.');
}
export function teardown(data) {
console.log('Load test complete.');
}
Run it:
k6 run --env BASE_URL=https://api.staging.example.com tests/load/checkout-flow.js
# With output to Grafana/InfluxDB for dashboards:
k6 run --out influxdb=http://localhost:8086/k6 tests/load/checkout-flow.js
Spike and Stress Testing
Write a spike test that hammers our API with 10x normal traffic suddenly.
Also a stress test that finds our breaking point.
// tests/load/spike-test.js
export const options = {
stages: [
{ duration: '10s', target: 10 }, // Normal traffic
{ duration: '30s', target: 10 }, // Normal traffic
{ duration: '10s', target: 200 }, // Spike! 20x
{ duration: '30s', target: 200 }, // Hold spike
{ duration: '10s', target: 10 }, // Back to normal
{ duration: '30s', target: 10 }, // Recovery check
{ duration: '10s', target: 0 },
],
thresholds: {
// During and after spike
http_req_failed: ['rate<0.05'], // Allow up to 5% errors during spike
http_req_duration: ['p(99)<2000'], // Can degrade to 2s during spike
},
};
// tests/load/stress-test.js — find the breaking point
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 600 },
{ duration: '2m', target: 800 },
{ duration: '5m', target: 0 },
],
// Don't set thresholds — we want to observe where it breaks, not fail early
};
CLAUDE.md for Performance Testing
## Performance Testing
- Load tests: tests/load/ using k6
- Run tests: k6 run tests/load/checkout-flow.js
- Staging URL: https://api.staging.example.com
- Test users: seeded by scripts/seed-load-test-users.ts (run before first test)
- SLOs: p99 < 500ms, error rate < 0.1%, checkout throughput > 50/s
- Results dashboard: http://grafana.internal/d/k6
- Never run load tests against production — staging only
Interpreting Results and Finding Bottlenecks
My k6 results show p99 = 2.3s on the product catalog endpoint.
95% of requests are under 300ms, but the tail is very long.
Help me find why.
Long-tail latency typically comes from one of three sources:
1. Database connection pool exhaustion
Run this while the load test is active:
SELECT count(*), wait_event_type, wait_event
FROM pg_stat_activity
WHERE state = 'active'
GROUP BY wait_event_type, wait_event
ORDER BY count DESC;
If you see Client wait events or high counts, your pool is too small:
// Increase pool size
const pool = new Pool({
max: 20, // Default is often 10 — increase for high concurrency
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000, // Fail fast rather than queue forever
});
2. Missing database indexes showing up at scale
-- Find slow queries during/after load test
SELECT query, mean_exec_time, calls, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
3. Memory pressure causing GC pauses
# Check Node.js memory during test
node --expose-gc --max-old-space-size=2048 server.js
# Add V8 metrics to k6 results
k6 run --env REPORT_GC=true tests/load/catalog.js
CI Integration
# .github/workflows/load-test.yml
name: Load test on staging
on:
deployment:
types: [created]
jobs:
k6-load-test:
runs-on: ubuntu-latest
if: github.event.deployment.environment == 'staging'
steps:
- uses: actions/checkout@v4
- name: Run k6 load test
uses: grafana/[email protected]
with:
filename: tests/load/checkout-flow.js
flags: --env BASE_URL=https://api.staging.example.com
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }} # Optional: cloud for larger tests
- name: Publish results
if: always()
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results.json
For SQL optimization to fix the database bottlenecks load testing reveals, see the SQL optimization guide. For observability to understand what’s happening under load, see the OpenTelemetry guide. The Claude Skills 360 bundle includes performance testing skill sets for k6, Gatling, and profiling workflows. Start with the free tier to try load test script generation.