name: load-testing description: Auto-activates when user mentions load test, performance test, stress test, k6, Artillery, benchmark, or scalability testing. Expert in designing and executing performance tests. category: testing
Load Testing & Performance Testing
Creates comprehensive load tests using k6, Artillery, and other tools to validate application performance under stress.
When This Activates
- User says: "load test this", "performance test", "stress test", "benchmark"
- User mentions: "k6", "Artillery", "JMeter", "Gatling", "scalability"
- Performance validation needed
- Pre-production testing
- Capacity planning questions
Load Testing Tools Comparison
k6 (Recommended)
- Best for: Modern apps, APIs, microservices
- Language: JavaScript/TypeScript
- Pros: Cloud-native, great metrics, scriptable
- Use when: Testing REST APIs, GraphQL, WebSocket
Artillery
- Best for: Quick tests, CI/CD integration
- Language: YAML + JavaScript
- Pros: Simple config, plugins, HTTP/WS/Socket.io
- Use when: Simple scenarios, rapid testing
Apache JMeter
- Best for: Enterprise apps, complex scenarios
- Language: GUI-based + Java
- Pros: Mature, extensive protocols, GUI
- Use when: Legacy systems, complex workflows
k6 Load Testing
Basic Load Test
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const checkDuration = new Trend('check_duration');
const requests = new Counter('requests');
// Test configuration
export const options = {
stages: [
{ duration: '2m', target: 10 }, // Ramp up to 10 users
{ duration: '5m', target: 10 }, // Stay at 10 users
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Stay at 50 users
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95% < 500ms, 99% < 1s
http_req_failed: ['rate<0.01'], // Error rate < 1%
errors: ['rate<0.1'], // Custom error rate < 10%
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
const API_TOKEN = __ENV.API_TOKEN || '';
export default function () {
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`,
},
tags: { name: 'GetUsers' },
};
// GET request
let response = http.get(`${BASE_URL}/api/users`, params);
const checkResult = check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has users': (r) => JSON.parse(r.body).data.length > 0,
});
errorRate.add(!checkResult);
checkDuration.add(response.timings.duration);
requests.add(1);
// POST request
const payload = JSON.stringify({
name: 'Test User',
email: `test-${Date.now()}@example.com`,
});
response = http.post(`${BASE_URL}/api/users`, payload, params);
check(response, {
'user created': (r) => r.status === 201,
});
sleep(1); // Think time between requests
}
export function handleSummary(data) {
return {
'summary.json': JSON.stringify(data),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}
Advanced Scenarios
// advanced-test.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
// Load test data
const users = new SharedArray('users', function () {
return papaparse.parse(open('./users.csv'), { header: true }).data;
});
export const options = {
scenarios: {
// Scenario 1: Constant load
constant_load: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
},
// Scenario 2: Ramping load
ramping_load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 100 },
{ duration: '10m', target: 100 },
{ duration: '5m', target: 0 },
],
gracefulRampDown: '30s',
},
// Scenario 3: Stress test
stress_test: {
executor: 'ramping-arrival-rate',
startRate: 50,
timeUnit: '1s',
preAllocatedVUs: 500,
maxVUs: 1000,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 500 },
{ duration: '5m', target: 500 },
{ duration: '2m', target: 0 },
],
},
},
};
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
group('User Flow', function () {
// 1. Login
group('Login', function () {
const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: user.email,
password: user.password,
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login successful': (r) => r.status === 200,
});
const token = loginRes.json('token');
// 2. Get profile
group('Get Profile', function () {
const profileRes = http.get(`${BASE_URL}/api/profile`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
check(profileRes, {
'profile loaded': (r) => r.status === 200,
});
});
// 3. Create post
group('Create Post', function () {
const postRes = http.post(`${BASE_URL}/api/posts`, JSON.stringify({
title: 'Load Test Post',
content: 'This is a test post from k6',
}), {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
check(postRes, {
'post created': (r) => r.status === 201,
});
});
});
});
sleep(Math.random() * 3 + 2); // Random think time 2-5s
}
GraphQL Load Test
// graphql-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 50,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<1000'],
},
};
const GRAPHQL_ENDPOINT = `${__ENV.BASE_URL}/graphql`;
export default function () {
const query = `
query GetPosts($first: Int!) {
posts(first: $first) {
edges {
node {
id
title
author {
name
}
}
}
}
}
`;
const variables = { first: 20 };
const response = http.post(
GRAPHQL_ENDPOINT,
JSON.stringify({ query, variables }),
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${__ENV.API_TOKEN}`,
},
}
);
check(response, {
'no errors': (r) => !r.json('errors'),
'has data': (r) => r.json('data.posts.edges.length') > 0,
});
}
Artillery Load Testing
Basic Configuration
# artillery.yml
config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 10 # 10 users per second
name: "Warm up"
- duration: 300
arrivalRate: 50 # 50 users per second
name: "Sustained load"
- duration: 120
arrivalRate: 100 # 100 users per second
name: "Spike"
processor: "./helpers.js"
variables:
apiToken: "{{ $processEnvironment.API_TOKEN }}"
plugins:
expect: {}
metrics-by-endpoint: {}
scenarios:
- name: "User Registration and Login"
flow:
- post:
url: "/api/auth/register"
json:
email: "user-{{ $randomString() }}@example.com"
name: "Test User"
password: "password123"
capture:
- json: "$.token"
as: "authToken"
expect:
- statusCode: 201
- contentType: json
- hasProperty: token
- get:
url: "/api/profile"
headers:
Authorization: "Bearer {{ authToken }}"
expect:
- statusCode: 200
- post:
url: "/api/posts"
headers:
Authorization: "Bearer {{ authToken }}"
json:
title: "Test Post"
content: "This is a test"
expect:
- statusCode: 201
- name: "Browse Posts"
weight: 3
flow:
- get:
url: "/api/posts?page=1&limit=20"
expect:
- statusCode: 200
- hasProperty: data
- think: 2 # Wait 2 seconds
- get:
url: "/api/posts/{{ $randomNumber(1, 100) }}"
expect:
- statusCode: [200, 404]
// helpers.js
module.exports = {
generateRandomEmail,
logResponse,
};
function generateRandomEmail(context, events, done) {
context.vars.email = `user-${Date.now()}@example.com`;
return done();
}
function logResponse(requestParams, response, context, ee, next) {
if (response.statusCode >= 400) {
console.error(`Error ${response.statusCode}: ${response.body}`);
}
return next();
}
Running Tests
# k6
k6 run load-test.js
k6 run --vus 100 --duration 5m load-test.js
k6 run --env BASE_URL=https://api.example.com load-test.js
# k6 Cloud
k6 cloud load-test.js
# Artillery
artillery run artillery.yml
artillery run --target https://api.example.com artillery.yml
# Artillery with environment
API_TOKEN=xxx artillery run artillery.yml
# Generate HTML report
artillery run --output report.json artillery.yml
artillery report report.json
CI/CD Integration
GitHub Actions
# .github/workflows/load-test.yml
name: Load Test
on:
schedule:
- cron: '0 2 * * *' # Run daily at 2 AM
workflow_dispatch:
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run k6 load test
uses: grafana/k6-action@v0.3.0
with:
filename: tests/load-test.js
cloud: true
token: ${{ secrets.K6_CLOUD_TOKEN }}
env:
BASE_URL: ${{ secrets.STAGING_URL }}
API_TOKEN: ${{ secrets.API_TOKEN }}
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: load-test-results
path: summary.json
Performance Benchmarking
// benchmark.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
baseline: {
executor: 'constant-vus',
vus: 1,
duration: '1m',
tags: { test_type: 'baseline' },
},
},
};
export default function () {
const endpoints = [
'/api/users',
'/api/posts',
'/api/comments',
];
endpoints.forEach(endpoint => {
const response = http.get(`${BASE_URL}${endpoint}`);
check(response, {
[`${endpoint} status 200`]: (r) => r.status === 200,
});
});
}
export function handleSummary(data) {
const baseline = {
endpoints: {},
timestamp: new Date().toISOString(),
};
for (const [name, metric] of Object.entries(data.metrics)) {
if (name.includes('http_req_duration')) {
baseline.endpoints[name] = {
avg: metric.values.avg,
p95: metric.values['p(95)'],
p99: metric.values['p(99)'],
};
}
}
return {
'baseline.json': JSON.stringify(baseline, null, 2),
};
}
Analyzing Results
Key Metrics to Monitor
-
Response Time
- Average, p95, p99
- Target: p95 < 500ms, p99 < 1s
-
Error Rate
- HTTP 4xx, 5xx errors
- Target: < 1%
-
Throughput
- Requests per second
- Target: Based on requirements
-
Concurrent Users
- Maximum sustainable load
- Target: Based on traffic projections
Identifying Bottlenecks
// Find slow endpoints
const slowRequests = data.metrics.http_req_duration.values;
console.log(`Average: ${slowRequests.avg}ms`);
console.log(`p95: ${slowRequests['p(95)']}ms`);
console.log(`p99: ${slowRequests['p(99)']}ms`);
// Check error patterns
const failedRequests = data.metrics.http_req_failed.values.rate;
if (failedRequests > 0.01) {
console.error(`Error rate: ${failedRequests * 100}%`);
}
Best Practices
Test Types
-
Smoke Test: 1-2 VUs, 1-2 minutes
- Verify system works under minimal load
-
Load Test: Expected normal/peak load, 5-15 minutes
- Validate performance under typical conditions
-
Stress Test: Beyond peak load, gradually increase
- Find breaking point
-
Spike Test: Sudden large increase, then drop
- Test auto-scaling, recovery
-
Soak Test: Moderate load, 1+ hours
- Find memory leaks, degradation
Checklist
- Define performance requirements (SLAs)
- Identify critical user flows
- Prepare test data (users, content)
- Set up realistic scenarios
- Configure appropriate thresholds
- Test in staging environment first
- Monitor system resources during tests
- Analyze results against baselines
- Document bottlenecks and fixes
- Run tests regularly (CI/CD)
- Compare results over time
Design load tests, execute tests, analyze results, provide optimization recommendations.