Modern Load Testing with k6
Performance testing with k6, a modern load testing tool
Introduction
k6 is a modern, developer-friendly load testing tool built for the DevOps era. Unlike older tools like JMeter, k6 lets you write tests in JavaScript, integrates seamlessly with CI/CD, and provides powerful analytics. This guide will teach you how to use k6 to ensure your applications perform under pressure.
Why k6?
Traditional Load Testing Problems:
- Complex GUI tools (JMeter, LoadRunner)
- Hard to version control
- Difficult to integrate with CI/CD
- Resource-intensive test runners
k6 Advantages:
- Write tests in JavaScript
- CLI-first, automation-friendly
- Built-in metrics and thresholds
- Efficient resource usage
- Great for DevOps workflows
When to Use k6
- Load Testing: Simulate normal user load
- Stress Testing: Find breaking points
- Spike Testing: Handle sudden traffic spikes
- Soak Testing: Long-running stability tests
- API Performance Testing: Benchmark API endpoints
Getting Started with k6
Installation
# macOS
brew install k6
# Windows
choco install k6
# Linux
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb 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
# Docker
docker pull grafana/k6Your First k6 Test
// simple-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
// Make HTTP request
const response = http.get('https://api.example.com/products');
// Verify response
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
// Think time between requests
sleep(1);
}Run the test:
# Run with 10 virtual users for 30 seconds
k6 run --vus 10 --duration 30s simple-test.jsOutput:
✓ status is 200
✓ response time < 500ms
checks.........................: 100.00% ✓ 300 ✗ 0
data_received..................: 1.2 MB 40 kB/s
data_sent......................: 30 kB 1.0 kB/s
http_req_blocked...............: avg=1.2ms min=0s med=1ms max=5ms p(90)=2ms p(95)=3ms
http_req_connecting............: avg=800µs min=0s med=700µs max=2ms p(90)=1.2ms p(95)=1.5ms
http_req_duration..............: avg=245ms min=180ms med=240ms max=420ms p(90)=290ms p(95)=320ms
http_req_receiving.............: avg=1.5ms min=0s med=1ms max=8ms p(90)=3ms p(95)=4ms
http_req_sending...............: avg=800µs min=0s med=700µs max=2ms p(90)=1.2ms p(95)=1.4ms
http_req_waiting...............: avg=243ms min=178ms med=238ms max=415ms p(90)=288ms p(95)=318ms
http_reqs......................: 300 10/s
iteration_duration.............: avg=1.24s min=1.18s med=1.24s max=1.42s p(90)=1.29s p(95)=1.32s
iterations.....................: 300 10/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10Load Testing Patterns
1. Constant Load
Maintain steady number of users:
// constant-load.js
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 50, // 50 virtual users
duration: '5m', // for 5 minutes
};
export default function () {
http.get('https://api.example.com/products');
sleep(1);
}2. Ramping Load
Gradually increase load:
// ramping-load.js
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 to 0
],
};
export default function () {
http.get('https://api.example.com/products');
sleep(1);
}3. Spike Testing
Sudden traffic increase:
// spike-test.js
export const options = {
stages: [
{ duration: '1m', target: 10 }, // Normal load
{ duration: '10s', target: 200 }, // Sudden spike!
{ duration: '3m', target: 200 }, // Stay at spike
{ duration: '10s', target: 10 }, // Drop back
{ duration: '1m', target: 10 }, // Recover
],
};
export default function () {
http.get('https://api.example.com/products');
sleep(1);
}4. Stress Testing
Find breaking point:
// stress-test.js
export const options = {
stages: [
{ duration: '2m', target: 50 }, // Normal load
{ duration: '5m', target: 50 },
{ duration: '2m', target: 100 }, // High load
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 }, // Very high
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 }, // Extreme load
{ duration: '5m', target: 300 },
{ duration: '5m', target: 0 }, // Recovery
],
thresholds: {
http_req_failed: ['rate<0.1'], // <10% errors
http_req_duration: ['p(95)<2000'], // 95% under 2s
},
};5. Soak Testing
Long-running stability test:
// soak-test.js
export const options = {
stages: [
{ duration: '5m', target: 50 }, // Ramp up
{ duration: '8h', target: 50 }, // Stay for 8 hours
{ duration: '5m', target: 0 }, // Ramp down
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};Real-World API Testing
Complete E-commerce API Test
// ecommerce-load-test.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const checkoutDuration = new Trend('checkout_duration');
// Configuration
export const options = {
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '1m', target: 0 },
],
thresholds: {
'http_req_duration': ['p(95)<500', 'p(99)<1000'],
'http_req_failed': ['rate<0.1'],
'errors': ['rate<0.1'],
'checkout_duration': ['p(95)<2000'],
},
};
const BASE_URL = 'https://api.example.com';
// Test data
const products = [
{ id: 'prod_1', name: 'Laptop' },
{ id: 'prod_2', name: 'Mouse' },
{ id: 'prod_3', name: 'Keyboard' },
];
export function setup() {
// Run once before test
// Create test user, setup data, etc.
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: 'loadtest@example.com',
password: 'Test1234!',
}), {
headers: { 'Content-Type': 'application/json' },
});
return { token: loginRes.json('token') };
}
export default function (data) {
const authHeaders = {
headers: {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json',
},
};
group('Browse Products', function () {
// List products
const listRes = http.get(`${BASE_URL}/products`);
check(listRes, {
'list products status 200': (r) => r.status === 200,
'list has products': (r) => r.json('products').length > 0,
}) || errorRate.add(1);
sleep(1);
// View product details
const productId = products[Math.floor(Math.random() * products.length)].id;
const detailRes = http.get(`${BASE_URL}/products/${productId}`);
check(detailRes, {
'product detail status 200': (r) => r.status === 200,
'product has name': (r) => r.json('name') !== undefined,
}) || errorRate.add(1);
sleep(2);
});
group('Shopping Cart', function () {
// Add to cart
const addRes = http.post(
`${BASE_URL}/cart/items`,
JSON.stringify({
productId: 'prod_1',
quantity: 1,
}),
authHeaders
);
check(addRes, {
'add to cart status 201': (r) => r.status === 201,
}) || errorRate.add(1);
sleep(1);
// View cart
const cartRes = http.get(`${BASE_URL}/cart`, authHeaders);
check(cartRes, {
'view cart status 200': (r) => r.status === 200,
'cart has items': (r) => r.json('items').length > 0,
}) || errorRate.add(1);
sleep(2);
});
group('Checkout', function () {
const checkoutStart = Date.now();
// Create order
const orderRes = http.post(
`${BASE_URL}/orders`,
JSON.stringify({
shippingAddress: {
street: '123 Test St',
city: 'Test City',
zip: '12345',
},
}),
authHeaders
);
check(orderRes, {
'create order status 201': (r) => r.status === 201,
}) || errorRate.add(1);
const orderId = orderRes.json('id');
sleep(1);
// Process payment
const paymentRes = http.post(
`${BASE_URL}/payments`,
JSON.stringify({
orderId: orderId,
method: 'credit_card',
cardNumber: '4242424242424242',
expiryMonth: '12',
expiryYear: '2027',
cvv: '123',
}),
authHeaders
);
check(paymentRes, {
'payment status 200': (r) => r.status === 200,
'payment successful': (r) => r.json('status') === 'succeeded',
}) || errorRate.add(1);
const checkoutTime = Date.now() - checkoutStart;
checkoutDuration.add(checkoutTime);
sleep(1);
});
// Think time between iterations
sleep(Math.random() * 5 + 5); // 5-10 seconds
}
export function teardown(data) {
// Cleanup after test
// Delete test data, logout, etc.
}Run with Custom Options
# Run with environment variables
k6 run \
-e BASE_URL=https://staging.example.com \
-e USERS=50 \
--out json=results.json \
ecommerce-load-test.js
# Run with cloud output
k6 run --out cloud ecommerce-load-test.js
# Run with InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 ecommerce-load-test.jsAdvanced Features
1. Data Parameterization
Use external data files:
// users.json
[
{"email": "user1@test.com", "password": "pass1"},
{"email": "user2@test.com", "password": "pass2"},
{"email": "user3@test.com", "password": "pass3"}
]// parameterized-test.js
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
// Load JSON data
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json'));
});
// Load CSV data
const products = new SharedArray('products', function () {
const csvData = open('./products.csv');
return papaparse.parse(csvData, { header: true }).data;
});
export default function () {
// Use data for each VU
const user = users[__VU % users.length];
const product = products[__ITER % products.length];
// Login with user
const loginRes = http.post(`${BASE_URL}/login`, JSON.stringify({
email: user.email,
password: user.password,
}));
// Get product
http.get(`${BASE_URL}/products/${product.id}`);
}2. Scenarios
Advanced execution patterns:
// scenarios.js
export const options = {
scenarios: {
// Constant load scenario
constant_load: {
executor: 'constant-vus',
vus: 10,
duration: '5m',
tags: { scenario: 'constant' },
},
// Ramping scenario
ramping_load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '1m', target: 0 },
],
gracefulRampDown: '30s',
tags: { scenario: 'ramping' },
},
// Fixed iterations
fixed_iterations: {
executor: 'shared-iterations',
vus: 10,
iterations: 200,
maxDuration: '10m',
},
// Per-VU iterations
per_vu_iterations: {
executor: 'per-vu-iterations',
vus: 10,
iterations: 20, // Each VU does 20 iterations
maxDuration: '10m',
},
// Constant arrival rate
constant_arrival_rate: {
executor: 'constant-arrival-rate',
rate: 30, // 30 iterations/second
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 100,
},
},
};3. Custom Metrics
Track business metrics:
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';
// Define custom metrics
const checkoutAttempts = new Counter('checkout_attempts');
const checkoutSuccesses = new Counter('checkout_successes');
const checkoutFailures = new Counter('checkout_failures');
const checkoutErrorRate = new Rate('checkout_error_rate');
const cartValue = new Trend('cart_value');
const activeUsers = new Gauge('active_users');
export default function () {
activeUsers.add(1);
// Simulate checkout
checkoutAttempts.add(1);
const response = http.post(`${BASE_URL}/checkout`, payload);
if (response.status === 200) {
checkoutSuccesses.add(1);
checkoutErrorRate.add(0);
cartValue.add(response.json('total'));
} else {
checkoutFailures.add(1);
checkoutErrorRate.add(1);
}
activeUsers.add(-1);
}4. Thresholds
Define pass/fail criteria:
export const options = {
thresholds: {
// HTTP errors < 1%
'http_req_failed': ['rate<0.01'],
// 95th percentile < 500ms
'http_req_duration': ['p(95)<500'],
// 99th percentile < 1s
'http_req_duration{endpoint:checkout}': ['p(99)<1000'],
// Average < 200ms
'http_req_duration': ['avg<200'],
// All checks must pass
'checks': ['rate>0.99'],
// Custom metric threshold
'checkout_error_rate': ['rate<0.05'],
// Abort test if threshold breached
'http_req_failed': [
{ threshold: 'rate<0.1', abortOnFail: true }
],
},
};5. Checks and Validation
Comprehensive response validation:
import { check } from 'k6';
export default function () {
const response = http.get(`${BASE_URL}/api/products/123`);
check(response, {
// Status checks
'status is 200': (r) => r.status === 200,
// Header checks
'has content-type': (r) => r.headers['Content-Type'] !== undefined,
'is json': (r) => r.headers['Content-Type'].includes('application/json'),
// Body checks
'has product id': (r) => r.json('id') === '123',
'has product name': (r) => r.json('name').length > 0,
'price is valid': (r) => r.json('price') > 0,
// Performance checks
'response time < 500ms': (r) => r.timings.duration < 500,
// Size checks
'response size < 1MB': (r) => r.body.length < 1000000,
// Pattern matching
'body contains product': (r) => r.body.includes('product'),
// Complex validation
'product is valid': (r) => {
const product = r.json();
return product.id && product.name && product.price > 0 && product.stock >= 0;
},
});
}6. Think Time and Pacing
Realistic user behavior:
import { sleep } from 'k6';
export default function () {
// Fixed sleep
http.get(`${BASE_URL}/products`);
sleep(1); // Wait 1 second
// Random sleep (realistic variation)
http.get(`${BASE_URL}/products/123`);
sleep(Math.random() * 3 + 2); // Random 2-5 seconds
// Think time distribution (more realistic)
function thinkTime() {
// Most users: 1-3 seconds
// Some users: up to 10 seconds
const rand = Math.random();
if (rand < 0.7) return 1 + Math.random() * 2; // 70% are quick
if (rand < 0.9) return 3 + Math.random() * 3; // 20% are medium
return 6 + Math.random() * 4; // 10% are slow
}
http.post(`${BASE_URL}/cart`, payload);
sleep(thinkTime());
}Integration and Visualization
CI/CD Integration
GitHub Actions:
name: Load Test
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run k6 test
uses: grafana/k6-action@v0.3.0
with:
filename: load-tests/api-test.js
flags: --out json=results.json
- name: Check thresholds
run: |
if grep -q '"thresholds_failed"' results.json; then
echo "Load test thresholds failed!"
exit 1
fi
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: k6-results
path: results.json
- name: Comment on PR
if: github.event_name == 'pull_request'
run: |
# Parse results and comment on PR
# (implementation details)Jenkins:
pipeline {
agent any
stages {
stage('Load Test') {
steps {
sh 'k6 run --out json=results.json load-test.js'
}
}
stage('Analyze Results') {
steps {
script {
def results = readJSON file: 'results.json'
if (results.root_group.checks[0].fails > 0) {
error('Load test checks failed')
}
}
}
}
}
post {
always {
archiveArtifacts 'results.json'
}
}
}Grafana Dashboard
Send metrics to InfluxDB and visualize in Grafana:
# Start InfluxDB
docker run -d -p 8086:8086 influxdb:1.8
# Run k6 with InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 test.jsGrafana Dashboard Panels:
- Request rate (requests/sec)
- Error rate (%)
- Response time percentiles (p50, p95, p99)
- Active VUs
- Data sent/received
- Check success rate
k6 Cloud
export const options = {
ext: {
loadimpact: {
projectID: 123456,
name: 'API Load Test',
distribution: {
'amazon:us:ashburn': { loadZone: 'amazon:us:ashburn', percent: 50 },
'amazon:eu:dublin': { loadZone: 'amazon:eu:dublin', percent: 50 },
},
},
},
};# Login to k6 cloud
k6 login cloud
# Run test in cloud
k6 cloud test.jsPerformance Debugging
Identify Slow Endpoints
import { group } from 'k6';
export default function () {
group('Fast Endpoint', function () {
http.get(`${BASE_URL}/health`); // Should be fast
});
group('Slow Endpoint', function () {
http.get(`${BASE_URL}/reports/heavy`); // Might be slow
});
}
// Results will show timing per group
// http_req_duration{group:::Fast Endpoint}: avg=50ms
// http_req_duration{group:::Slow Endpoint}: avg=2000msDatabase Connection Pooling
// Test database under load
export const options = {
stages: [
{ duration: '1m', target: 100 },
],
};
export default function () {
const response = http.get(`${BASE_URL}/api/users`);
check(response, {
'no connection errors': (r) =>
!r.body.includes('too many connections') &&
!r.body.includes('connection pool exhausted'),
});
}Memory Leak Detection
// Soak test to find memory leaks
export const options = {
stages: [
{ duration: '5m', target: 50 },
{ duration: '2h', target: 50 }, // Long run
],
};
// Monitor server memory while this runs
// If memory grows continuously, there's a leakBest Practices
- Start Small: Begin with low load, increase gradually
- Use Realistic Data: Test with production-like data volumes
- Think Time: Add realistic delays between requests
- Monitor Everything: Track both k6 metrics and server metrics
- Set Thresholds: Define clear pass/fail criteria
- Test in Isolation: One scenario per test for clarity
- Version Control: Store tests in git alongside code
- Schedule Regular Tests: Catch performance regressions early
- Test in Staging First: Don't load test production without permission
- Document Baselines: Know your normal performance levels
Common Pitfalls
- Testing from Same Network: Creates artificial bottlenecks
- Ignoring Think Time: Unrealistic traffic pattern
- Too Many VUs: k6 running machine becomes bottleneck
- Not Checking Errors: High throughput but all errors
- Testing Wrong Environment: Staging != Production
- No Warmup: Cold start skews results
- Insufficient Duration: Need time to reach steady state
Next Steps
- Install k6 and run your first test
- Create baseline tests for key user journeys
- Set up dashboards in Grafana
- Integrate with CI/CD to run on every deploy
- Schedule soak tests to run nightly
- Learn k6 browser for frontend performance testing
- Explore k6 extensions for custom protocols
Related Articles
- "Performance Testing Fundamentals" - Core concepts
- "Monitoring & Observability" - Track production performance
- "API Testing Strategies" - What to test
- "CI/CD Integration" - Automate performance testing
- "Chaos Engineering" - Performance under failure
Conclusion
k6 makes performance testing accessible to developers and QE engineers. By writing tests as code, you can:
- Version control your load tests
- Run them in CI/CD pipelines
- Catch performance regressions early
- Scale testing across regions
- Make performance a first-class citizen
Start with simple tests of critical endpoints, establish baselines, and gradually expand coverage. Performance issues are easier to fix before they reach production!
Remember: You don't know if your system can handle 10,000 users until you test it with 10,000 users!
Comments (0)
Loading comments...