From qa-essentials
Load testing skill using k6 to write scripts with thresholds, scenarios, checks, custom metrics, and results analysis.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-essentials:k6-performanceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are an expert performance engineer specializing in k6 load testing. When the user asks you to write, review, or debug k6 performance tests, follow these detailed instructions.
You are an expert performance engineer specializing in k6 load testing. When the user asks you to write, review, or debug k6 performance tests, follow these detailed instructions.
k6/
scripts/
smoke-test.js
load-test.js
stress-test.js
spike-test.js
soak-test.js
scenarios/
api-scenarios.js
user-flows.js
utils/
helpers.js
auth.js
data-generators.js
data/
users.csv
payloads.json
thresholds/
default-thresholds.js
config/
environments.js
results/
.gitkeep
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const loginDuration = new Trend('login_duration');
const requestCount = new Counter('total_requests');
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: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th percentile < 500ms
http_req_failed: ['rate<0.01'], // Error rate < 1%
errors: ['rate<0.05'], // Custom error rate < 5%
login_duration: ['p(95)<800'], // Login 95th < 800ms
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
group('Homepage', () => {
const response = http.get(`${BASE_URL}/`);
check(response, {
'homepage status is 200': (r) => r.status === 200,
'homepage loads in < 2s': (r) => r.timings.duration < 2000,
'homepage has correct title': (r) => r.body.includes('<title>'),
});
errorRate.add(response.status !== 200);
requestCount.add(1);
});
sleep(1);
group('Login', () => {
const startTime = Date.now();
const loginResponse = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: '[email protected]',
password: 'SecurePass123!',
}), {
headers: { 'Content-Type': 'application/json' },
});
loginDuration.add(Date.now() - startTime);
check(loginResponse, {
'login status is 200': (r) => r.status === 200,
'login returns token': (r) => JSON.parse(r.body).token !== undefined,
});
errorRate.add(loginResponse.status !== 200);
requestCount.add(1);
});
sleep(Math.random() * 3 + 1); // Random think time between 1-4 seconds
}
export const options = {
vus: 1,
duration: '1m',
thresholds: {
http_req_duration: ['p(99)<1500'],
http_req_failed: ['rate<0.01'],
},
};
// Quick validation that the system works under minimal load
export default function () {
const response = http.get(`${BASE_URL}/api/health`);
check(response, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
export const options = {
stages: [
{ duration: '5m', target: 100 }, // Ramp up
{ duration: '10m', target: 100 }, // Steady state
{ duration: '5m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 },
{ duration: '5m', target: 300 },
{ duration: '2m', target: 400 },
{ duration: '5m', target: 400 },
{ duration: '10m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<1000'],
http_req_failed: ['rate<0.05'],
},
};
export const options = {
stages: [
{ duration: '1m', target: 10 }, // Normal load
{ duration: '10s', target: 500 }, // Spike!
{ duration: '3m', target: 500 }, // Stay at spike
{ duration: '10s', target: 10 }, // Recovery
{ duration: '3m', target: 10 }, // Observe recovery
{ duration: '1m', target: 0 }, // Ramp down
],
};
export const options = {
stages: [
{ duration: '5m', target: 50 }, // Ramp up
{ duration: '4h', target: 50 }, // Sustained load for 4 hours
{ duration: '5m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export const options = {
scenarios: {
browse_products: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
gracefulRampDown: '30s',
exec: 'browseProducts',
},
checkout_flow: {
executor: 'constant-arrival-rate',
rate: 10, // 10 iterations per timeUnit
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 20,
maxVUs: 50,
exec: 'checkoutFlow',
},
api_health_check: {
executor: 'constant-vus',
vus: 5,
duration: '10m',
exec: 'healthCheck',
},
},
thresholds: {
'http_req_duration{scenario:browse_products}': ['p(95)<300'],
'http_req_duration{scenario:checkout_flow}': ['p(95)<800'],
'http_req_duration{scenario:api_health_check}': ['p(95)<100'],
},
};
export function browseProducts() {
http.get(`${BASE_URL}/api/products`);
sleep(2);
}
export function checkoutFlow() {
// Full checkout flow
const cart = http.post(`${BASE_URL}/api/cart`, JSON.stringify({
productId: 'prod-001',
quantity: 1,
}), { headers: { 'Content-Type': 'application/json' } });
check(cart, { 'cart created': (r) => r.status === 201 });
const checkout = http.post(`${BASE_URL}/api/checkout`, JSON.stringify({
cartId: JSON.parse(cart.body).id,
}), { headers: { 'Content-Type': 'application/json' } });
check(checkout, { 'checkout success': (r) => r.status === 200 });
sleep(1);
}
export function healthCheck() {
http.get(`${BASE_URL}/api/health`);
sleep(1);
}
import http from 'k6/http';
import { check } from 'k6';
// Setup function runs once before the test
export function setup() {
const loginResponse = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: '[email protected]',
password: 'SecurePass123!',
}), {
headers: { 'Content-Type': 'application/json' },
});
const body = JSON.parse(loginResponse.body);
return { token: body.token };
}
export default function (data) {
const params = {
headers: {
Authorization: `Bearer ${data.token}`,
'Content-Type': 'application/json',
},
};
const response = http.get(`${BASE_URL}/api/users/me`, params);
check(response, {
'authenticated request succeeds': (r) => r.status === 200,
});
}
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { open } from 'k6';
const csvData = new SharedArray('users', function () {
return papaparse.parse(open('./data/users.csv'), { header: true }).data;
});
export default function () {
const user = csvData[Math.floor(Math.random() * csvData.length)];
const response = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: user.email,
password: user.password,
}), {
headers: { 'Content-Type': 'application/json' },
});
check(response, {
'login successful': (r) => r.status === 200,
});
}
import { SharedArray } from 'k6/data';
import { open } from 'k6';
const products = new SharedArray('products', function () {
return JSON.parse(open('./data/payloads.json'));
});
export default function () {
const product = products[__VU % products.length];
const response = http.post(`${BASE_URL}/api/products`, JSON.stringify(product), {
headers: { 'Content-Type': 'application/json' },
});
check(response, {
'product created': (r) => r.status === 201,
});
}
import { Trend, Rate, Counter, Gauge } from 'k6/metrics';
// Trend -- tracks min, max, avg, percentiles
const apiCallDuration = new Trend('api_call_duration');
// Rate -- tracks percentage of non-zero values
const failureRate = new Rate('failure_rate');
// Counter -- tracks cumulative count
const totalRequests = new Counter('total_requests');
// Gauge -- tracks last value
const activeUsers = new Gauge('active_users');
export default function () {
const start = Date.now();
const response = http.get(`${BASE_URL}/api/products`);
const duration = Date.now() - start;
apiCallDuration.add(duration);
failureRate.add(response.status !== 200);
totalRequests.add(1);
activeUsers.add(__VU);
}
sleep() between requests to model real users.group() for logical sections -- Groups appear in results and help analysis.check() extensively -- Checks validate correctness under load.SharedArray for large datasets -- It reduces memory usage across VUs.--out json=results.json for post-analysis.sleep() creates unrealistic load patterns.setup()/teardown() -- Use lifecycle hooks for test data management.open() outside the default function.# Basic run
k6 run scripts/load-test.js
# With environment variables
k6 run -e BASE_URL=https://staging.example.com scripts/load-test.js
# With output to JSON
k6 run --out json=results/output.json scripts/load-test.js
# With cloud output (k6 Cloud)
k6 cloud scripts/load-test.js
# With InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 scripts/load-test.js
# Override VUs and duration
k6 run --vus 50 --duration 5m scripts/smoke-test.js
After a test run, analyze these key metrics:
Look for these patterns:
npx claudepluginhub pramoddutta/qaskills --plugin qa-essentialsWrites and debugs k6 load test scripts in JavaScript/TypeScript covering all test types, thresholds, checks, scenarios, executors, and CI/CD integration.
Writes and executes k6 load tests for HTTP APIs, WebSocket endpoints, and browser scenarios. Configures smoke, load, stress, spike, and soak tests with thresholds and CI/CD integration.
Generates k6, Artillery, wrk scripts for API load/stress/soak tests to validate performance, identify bottlenecks, and establish baselines under configurable loads.