load-test-builder
SKILL.md
Load Test Builder
Validate system performance under realistic and stress conditions.
Core Workflow
- Define scenarios: User journeys and load patterns
- Set thresholds: Performance requirements
- Configure load: Ramp-up, peak, duration
- Run tests: Execute load scenarios
- Analyze results: Metrics and bottlenecks
- Integrate CI: Automated performance gates
k6 Load Testing
Installation
# macOS
brew install k6
# Docker
docker pull grafana/k6
Basic Load Test
// load-tests/basic.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
// Test configuration
export const options = {
stages: [
{ duration: '1m', target: 20 }, // Ramp up to 20 users
{ duration: '3m', target: 20 }, // Stay at 20 users
{ duration: '1m', target: 50 }, // Ramp up to 50 users
{ duration: '3m', target: 50 }, // Stay at 50 users
{ duration: '1m', target: 0 }, // Ramp down to 0
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
errors: ['rate<0.05'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
// Homepage
const homeResponse = http.get(`${BASE_URL}/`);
check(homeResponse, {
'homepage status is 200': (r) => r.status === 200,
'homepage loads fast': (r) => r.timings.duration < 500,
});
responseTime.add(homeResponse.timings.duration);
errorRate.add(homeResponse.status !== 200);
sleep(1);
// API request
const apiResponse = http.get(`${BASE_URL}/api/users`);
check(apiResponse, {
'api status is 200': (r) => r.status === 200,
'api returns array': (r) => Array.isArray(JSON.parse(r.body)),
});
errorRate.add(apiResponse.status !== 200);
sleep(Math.random() * 3 + 1); // Random think time 1-4 seconds
}
User Journey Test
// load-tests/user-journey.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', function () {
return JSON.parse(open('./data/users.json'));
});
export const options = {
scenarios: {
browse_and_buy: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
gracefulRampDown: '30s',
},
},
thresholds: {
'group_duration{group:::Login}': ['p(95)<2000'],
'group_duration{group:::Browse Products}': ['p(95)<1000'],
'group_duration{group:::Checkout}': ['p(95)<3000'],
http_req_failed: ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
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,
'has token': (r) => JSON.parse(r.body).token !== undefined,
});
if (loginRes.status !== 200) return;
const token = JSON.parse(loginRes.body).token;
group('Browse Products', function () {
const productsRes = http.get(`${BASE_URL}/api/products`, {
headers: { Authorization: `Bearer ${token}` },
});
check(productsRes, {
'products loaded': (r) => r.status === 200,
});
sleep(2);
// View product detail
const products = JSON.parse(productsRes.body);
if (products.length > 0) {
const productId = products[Math.floor(Math.random() * products.length)].id;
const productRes = http.get(`${BASE_URL}/api/products/${productId}`, {
headers: { Authorization: `Bearer ${token}` },
});
check(productRes, {
'product detail loaded': (r) => r.status === 200,
});
}
});
sleep(1);
group('Checkout', function () {
// Add to cart
const cartRes = http.post(
`${BASE_URL}/api/cart`,
JSON.stringify({ productId: '1', quantity: 1 }),
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}
);
check(cartRes, {
'added to cart': (r) => r.status === 200 || r.status === 201,
});
// Checkout
const checkoutRes = http.post(
`${BASE_URL}/api/checkout`,
JSON.stringify({ paymentMethod: 'card' }),
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}
);
check(checkoutRes, {
'checkout successful': (r) => r.status === 200 || r.status === 201,
});
});
});
sleep(Math.random() * 5 + 2);
}
Stress Test
// load-tests/stress.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Normal load
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 }, // High load
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 }, // Stress
{ duration: '5m', target: 300 },
{ duration: '2m', target: 400 }, // Breaking point
{ duration: '5m', target: 400 },
{ duration: '10m', target: 0 }, // Recovery
],
thresholds: {
http_req_duration: ['p(99)<1500'],
http_req_failed: ['rate<0.05'],
},
};
export default function () {
const response = http.get(`${__ENV.BASE_URL}/api/health`);
check(response, {
'status is 200': (r) => r.status === 200,
});
}
Spike Test
// load-tests/spike.js
export const options = {
stages: [
{ duration: '10s', target: 100 }, // Quick ramp
{ duration: '1m', target: 100 }, // Normal
{ duration: '10s', target: 1000 }, // Spike!
{ duration: '3m', target: 1000 }, // Stay at spike
{ duration: '10s', target: 100 }, // Scale down
{ duration: '3m', target: 100 }, // Recovery
{ duration: '10s', target: 0 }, // Ramp down
],
};
Soak Test
// load-tests/soak.js
export const options = {
stages: [
{ duration: '5m', target: 100 }, // Ramp up
{ duration: '8h', target: 100 }, // Sustained load for 8 hours
{ duration: '5m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
Artillery
Installation
npm install -D artillery
Configuration
# artillery/load-test.yml
config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 5
name: "Warm up"
- duration: 120
arrivalRate: 10
name: "Normal load"
- duration: 60
arrivalRate: 50
name: "Spike"
- duration: 60
arrivalRate: 10
name: "Cool down"
defaults:
headers:
Content-Type: "application/json"
plugins:
expect: {}
ensure:
p99: 500
maxErrorRate: 1
scenarios:
- name: "User Journey"
flow:
- get:
url: "/"
expect:
- statusCode: 200
- contentType: text/html
- think: 2
- get:
url: "/api/products"
expect:
- statusCode: 200
capture:
- json: "$[0].id"
as: "productId"
- think: 1
- get:
url: "/api/products/{{ productId }}"
expect:
- statusCode: 200
- post:
url: "/api/cart"
json:
productId: "{{ productId }}"
quantity: 1
expect:
- statusCode: 201
With Custom Functions
// artillery/processor.js
module.exports = {
generateUser,
logResponse,
validateCheckout,
};
function generateUser(context, events, done) {
context.vars.email = `user${Date.now()}@example.com`;
context.vars.password = 'testpassword123';
return done();
}
function logResponse(requestParams, response, context, events, done) {
console.log(`Response: ${response.statusCode} - ${response.body}`);
return done();
}
function validateCheckout(requestParams, response, context, events, done) {
const body = JSON.parse(response.body);
if (!body.orderId) {
return done(new Error('Missing orderId in response'));
}
context.vars.orderId = body.orderId;
return done();
}
# artillery/with-processor.yml
config:
target: "http://localhost:3000"
processor: "./processor.js"
phases:
- duration: 60
arrivalRate: 10
scenarios:
- name: "Checkout flow"
flow:
- function: "generateUser"
- post:
url: "/api/auth/register"
json:
email: "{{ email }}"
password: "{{ password }}"
- post:
url: "/api/checkout"
afterResponse: "validateCheckout"
Autocannon (Node.js)
// load-tests/autocannon.ts
import autocannon from 'autocannon';
async function runLoadTest() {
const result = await autocannon({
url: 'http://localhost:3000/api/users',
connections: 100,
duration: 30,
pipelining: 10,
headers: {
'Content-Type': 'application/json',
},
requests: [
{
method: 'GET',
path: '/api/users',
},
{
method: 'POST',
path: '/api/users',
body: JSON.stringify({ name: 'Test', email: 'test@example.com' }),
},
],
});
console.log(autocannon.printResult(result));
// Validate results
if (result.latency.p99 > 500) {
console.error('P99 latency exceeded 500ms');
process.exit(1);
}
if (result.errors > 0) {
console.error(`Errors detected: ${result.errors}`);
process.exit(1);
}
}
runLoadTest();
CI Integration
# .github/workflows/load-tests.yml
name: Load Tests
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
workflow_dispatch:
jobs:
load-test:
runs-on: ubuntu-latest
services:
app:
image: myapp:latest
ports:
- 3000:3000
env:
DATABASE_URL: postgresql://test@localhost/test
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Wait for app
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 1; done'
- name: Run load tests
run: k6 run --out json=results.json load-tests/basic.js
env:
BASE_URL: http://localhost:3000
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: load-test-results
path: results.json
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));
// Parse and format results for PR comment
Results Analysis
// scripts/analyze-results.js
import fs from 'fs';
const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));
const summary = {
totalRequests: results.metrics.http_reqs.count,
avgDuration: results.metrics.http_req_duration.avg,
p95Duration: results.metrics.http_req_duration['p(95)'],
p99Duration: results.metrics.http_req_duration['p(99)'],
errorRate: results.metrics.http_req_failed.rate,
throughput: results.metrics.http_reqs.rate,
};
console.table(summary);
// Check thresholds
const passed =
summary.p95Duration < 500 &&
summary.p99Duration < 1000 &&
summary.errorRate < 0.01;
process.exit(passed ? 0 : 1);
Best Practices
- Start small: Gradually increase load
- Use realistic data: Production-like scenarios
- Monitor everything: Backend metrics too
- Set thresholds: Define pass/fail criteria
- Test regularly: Catch regressions early
- Isolate environment: Dedicated test environment
- Document findings: Track improvements
- Include think time: Simulate real users
Output Checklist
Every load test setup should include:
- k6/Artillery configuration
- Realistic user scenarios
- Multiple load patterns (normal, stress, spike)
- Performance thresholds
- Custom metrics
- CI integration
- Results analysis
- Soak test for memory leaks
- Documentation
- Baseline benchmarks
Weekly Installs
11
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code9
gemini-cli8
antigravity8
windsurf8
github-copilot8
codex8