K6 is a modern, developer-friendly load testing tool designed for testing the performance and reliability of APIs, microservices, and websites. Built with Go and scriptable in JavaScript, K6 brings performance testing into the DevOps workflow with seamless CI/CD integration and cloud-native capabilities. For comprehensive performance testing strategies, explore our performance testing guide.
Why K6 for Modern Load Testing?
K6 addresses the limitations of traditional performance testing tools by focusing on developer experience and modern infrastructure:
- JavaScript API: Familiar scripting for frontend and backend developers
- CLI-first design: Perfect for automation and CI/CD pipelines
- Performance: Efficient resource usage, can generate high load from single machine
- Cloud & Local: Run locally during development, scale to cloud for production-level testing
- Modern protocols: HTTP/1.1, HTTP/2, WebSockets, gRPC support
- Observability: Real-time metrics, Prometheus integration, Grafana dashboards
K6 vs Traditional Load Testing Tools
For detailed comparisons, see our guides on JMeter, Gatling, and Locust.
Feature | K6 | JMeter | Gatling | Locust |
---|---|---|---|---|
Scripting Language | JavaScript | GUI/XML | Scala | Python |
Resource Efficiency | Excellent | Medium | Good | Medium |
CI/CD Integration | Native | Plugins | Good | Good |
Learning Curve | Low | Medium | High | Low |
Cloud Support | Native (K6 Cloud) | Plugins | Gatling Enterprise | No native |
Protocol Support | HTTP/2, WS, gRPC | Extensive | HTTP/2, WS | HTTP |
Real-time Results | Excellent | Limited | Good | Good |
Getting Started with K6
Installation
# macOS (Homebrew)
brew install k6
# Windows (Chocolatey)
choco install k6
# Linux (Debian/Ubuntu)
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
# Docker
docker pull grafana/k6
Your First K6 Test
// simple-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // Virtual users
duration: '30s', // Test duration
};
export default function () {
const response = http.get('https://test-api.k6.io');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Run the test:
k6 run simple-test.js
Test Configuration and Load Profiles
Virtual Users and Duration
export const options = {
// Simple configuration
vus: 50,
duration: '5m',
// Or use stages for ramping
stages: [
{ 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 users
],
};
Load Testing Scenarios
export const options = {
scenarios: {
// Constant load
constant_load: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
},
// Ramping VUs
ramping_load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 50 },
{ duration: '1m', target: 0 },
],
},
// Constant request rate
constant_rps: {
executor: 'constant-arrival-rate',
rate: 1000, // 1000 requests per second
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 200,
},
// Spike test
spike_test: {
executor: 'ramping-arrival-rate',
startRate: 100,
timeUnit: '1s',
preAllocatedVUs: 50,
maxVUs: 500,
stages: [
{ duration: '30s', target: 100 }, // Normal load
{ duration: '1m', target: 1000 }, // Spike
{ duration: '30s', target: 100 }, // Back to normal
],
},
},
};
Thresholds for Pass/Fail Criteria
export const options = {
thresholds: {
// HTTP errors should be less than 1%
http_req_failed: ['rate<0.01'],
// 95% of requests should be below 500ms
http_req_duration: ['p(95)<500'],
// 99th percentile should be below 1s
'http_req_duration{type:api}': ['p(99)<1000'],
// Custom metric thresholds
checks: ['rate>0.99'], // 99% of checks should pass
},
};
Advanced HTTP Testing
HTTP Methods and Request Bodies
import http from 'k6/http';
import { check } from 'k6';
export default function () {
// GET request
const getResponse = http.get('https://api.example.com/users');
// POST with JSON
const payload = JSON.stringify({
name: 'John Doe',
email: 'john@example.com',
});
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
};
const postResponse = http.post(
'https://api.example.com/users',
payload,
params
);
check(postResponse, {
'created successfully': (r) => r.status === 201,
'has user id': (r) => r.json('id') !== undefined,
});
// PUT request
const updatePayload = JSON.stringify({ name: 'Jane Doe' });
http.put(`https://api.example.com/users/${postResponse.json('id')}`, updatePayload, params);
// DELETE request
http.del(`https://api.example.com/users/${postResponse.json('id')}`, null, params);
}
Batch Requests
import http from 'k6/http';
export default function () {
// Execute requests in parallel
const responses = http.batch([
['GET', 'https://api.example.com/users'],
['GET', 'https://api.example.com/products'],
['GET', 'https://api.example.com/orders'],
]);
// Check all responses
responses.forEach((response, index) => {
check(response, {
[`request ${index} status is 200`]: (r) => r.status === 200,
});
});
}
Authentication and Session Management
import http from 'k6/http';
import { check } from 'k6';
export function setup() {
// Login once per test run
const loginResponse = http.post('https://api.example.com/auth/login', {
username: 'testuser',
password: 'testpass',
});
return { token: loginResponse.json('token') };
}
export default function (data) {
// Use token from setup
const params = {
headers: {
'Authorization': `Bearer ${data.token}`,
},
};
const response = http.get('https://api.example.com/protected', params);
check(response, {
'authenticated': (r) => r.status === 200,
});
}
Data Parameterization
Using CSV Data
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
const csvData = new SharedArray('users', function () {
return papaparse.parse(open('./users.csv'), { header: true }).data;
});
export default function () {
// Get random user
const user = csvData[Math.floor(Math.random() * csvData.length)];
const response = http.post('https://api.example.com/login', {
username: user.username,
password: user.password,
});
check(response, {
'login successful': (r) => r.status === 200,
});
}
Using JSON Data
import { SharedArray } from 'k6/data';
const testData = new SharedArray('products', function () {
return JSON.parse(open('./products.json'));
});
export default function () {
const product = testData[__VU % testData.length]; // Round-robin selection
http.post('https://api.example.com/cart/add', JSON.stringify({
productId: product.id,
quantity: 1,
}), {
headers: { 'Content-Type': 'application/json' },
});
}
Custom Metrics and Tags
Custom Metrics
import http from 'k6/http';
import { Trend, Counter, Rate, Gauge } from 'k6/metrics';
const myTrend = new Trend('waiting_time');
const myCounter = new Counter('my_counter');
const myRate = new Rate('my_rate');
const myGauge = new Gauge('my_gauge');
export default function () {
const response = http.get('https://api.example.com');
// Add custom metrics
myTrend.add(response.timings.waiting);
myCounter.add(1);
myRate.add(response.status === 200);
myGauge.add(response.timings.duration);
}
Tags for Organizing Metrics
import http from 'k6/http';
import { check } from 'k6';
export default function () {
// Tag individual requests
const response = http.get('https://api.example.com/users', {
tags: {
type: 'api',
endpoint: 'users',
},
});
check(response, {
'status 200': (r) => r.status === 200,
}, { type: 'api' }); // Tag checks
}
export const options = {
thresholds: {
// Threshold for specific tags
'http_req_duration{type:api}': ['p(95)<500'],
'http_req_duration{endpoint:users}': ['p(99)<1000'],
},
};
Real-World Testing Scenarios
E-commerce User Flow
This scenario demonstrates comprehensive load testing similar to patterns in API performance testing:
import http from 'k6/http';
import { check, group, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
'http_req_duration{page:home}': ['p(95)<300'],
'http_req_duration{page:product}': ['p(95)<400'],
},
};
export default function () {
let response;
group('Homepage', function () {
response = http.get('https://ecommerce.example.com', {
tags: { page: 'home' },
});
check(response, { 'homepage loaded': (r) => r.status === 200 });
sleep(1);
});
group('Search Products', function () {
response = http.get('https://ecommerce.example.com/search?q=laptop', {
tags: { page: 'search' },
});
check(response, { 'search results': (r) => r.status === 200 });
sleep(2);
});
group('View Product', function () {
response = http.get('https://ecommerce.example.com/products/123', {
tags: { page: 'product' },
});
check(response, { 'product details': (r) => r.status === 200 });
sleep(3);
});
group('Add to Cart', function () {
response = http.post(
'https://ecommerce.example.com/cart',
JSON.stringify({ productId: 123, quantity: 1 }),
{
headers: { 'Content-Type': 'application/json' },
tags: { page: 'cart' },
}
);
check(response, { 'added to cart': (r) => r.status === 200 });
sleep(1);
});
group('Checkout', function () {
response = http.post(
'https://ecommerce.example.com/checkout',
JSON.stringify({
payment: 'card',
address: '123 Main St',
}),
{
headers: { 'Content-Type': 'application/json' },
tags: { page: 'checkout' },
}
);
check(response, { 'checkout successful': (r) => r.status === 200 });
});
}
API Load Test with Error Handling
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const errorRate = new Rate('errors');
export const options = {
scenarios: {
stress_test: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
preAllocatedVUs: 50,
maxVUs: 500,
stages: [
{ duration: '1m', target: 10 },
{ duration: '2m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '1m', target: 10 },
],
},
},
thresholds: {
errors: ['rate<0.1'], // Error rate should be less than 10%
http_req_duration: ['p(95)<2000', 'p(99)<5000'],
},
};
export default function () {
const response = http.get('https://api.example.com/data');
const success = check(response, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 1000,
'no errors in body': (r) => !r.body.includes('error'),
});
errorRate.add(!success);
if (response.status !== 200) {
console.error(`Request failed: ${response.status} ${response.body}`);
}
sleep(1);
}
CI/CD Integration
GitHub Actions
name: Load Test
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
k6_load_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run K6 Load Test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/load-test.js
cloud: true
token: ${{ secrets.K6_CLOUD_TOKEN }}
- name: Upload Results
uses: actions/upload-artifact@v3
if: always()
with:
name: k6-results
path: results/
Jenkins Pipeline
pipeline {
agent any
stages {
stage('Load Test') {
steps {
sh '''
k6 run --out json=results.json \
--out influxdb=http://influxdb:8086/k6 \
tests/load-test.js
'''
}
}
stage('Check Thresholds') {
steps {
script {
def exitCode = sh(
script: 'k6 run --quiet tests/load-test.js',
returnStatus: true
)
if (exitCode != 0) {
error("Load test thresholds failed")
}
}
}
}
}
post {
always {
archiveArtifacts artifacts: 'results.json', fingerprint: true
}
}
}
GitLab CI
k6-test:
stage: test
image: grafana/k6:latest
script:
- k6 run --summary-export=summary.json tests/load-test.js
artifacts:
when: always
paths:
- summary.json
reports:
junit: summary.json
Observability and Results Analysis
Output Options
# JSON output
k6 run --out json=results.json script.js
# CSV output
k6 run --out csv=results.csv script.js
# InfluxDB + Grafana
k6 run --out influxdb=http://localhost:8086/k6 script.js
# Prometheus Remote Write
k6 run --out experimental-prometheus-rw script.js
# K6 Cloud
k6 run --out cloud script.js
# Multiple outputs
k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 script.js
Reading Results
# Summary statistics
k6 run script.js
# Detailed output
k6 run --verbose script.js
# Quiet mode (only errors)
k6 run --quiet script.js
# Save summary to file
k6 run --summary-export=summary.json script.js
Custom Summary
import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js";
export function handleSummary(data) {
return {
"summary.html": htmlReport(data),
"summary.json": JSON.stringify(data),
stdout: textSummary(data, { indent: " ", enableColors: true }),
};
}
Best Practices
Resource Efficiency
// ❌ Bad: Creates new object every iteration
export default function () {
const headers = { 'Content-Type': 'application/json' };
http.get('https://api.example.com', { headers });
}
// ✓ Good: Reuse objects
const params = {
headers: { 'Content-Type': 'application/json' },
};
export default function () {
http.get('https://api.example.com', params);
}
Think Time and Pacing
import { sleep } from 'k6';
export default function () {
// Simulate user think time
http.get('https://api.example.com/page1');
sleep(Math.random() * 3 + 2); // 2-5 seconds
http.get('https://api.example.com/page2');
sleep(Math.random() * 2 + 1); // 1-3 seconds
}
Modular Test Scripts
// modules/auth.js
export function login(username, password) {
const response = http.post('https://api.example.com/login', {
username,
password,
});
return response.json('token');
}
// modules/users.js
export function getUser(token, userId) {
return http.get(`https://api.example.com/users/${userId}`, {
headers: { Authorization: `Bearer ${token}` },
});
}
// main-test.js
import { login } from './modules/auth.js';
import { getUser } from './modules/users.js';
export default function () {
const token = login('user', 'pass');
getUser(token, 123);
}
Conclusion
K6 represents a paradigm shift in performance testing, bringing developer-friendly tooling and modern practices to load testing. Its JavaScript API, efficient resource usage, and seamless integration with CI/CD pipelines make it ideal for DevOps teams practicing continuous testing. Combine K6 with monitoring and observability tools for comprehensive performance validation.
Key advantages:
- Developer-friendly JavaScript API
- Efficient resource usage for high load generation
- Native CI/CD integration
- Modern protocol support (HTTP/2, WebSockets, gRPC)
- Flexible execution modes (local, cloud, hybrid)
- Rich ecosystem and observability options
Whether you’re testing microservices, APIs, or full web applications, K6 provides the tools and flexibility to ensure your systems perform reliably under load. By integrating performance testing early in the development cycle, teams can catch issues before they reach production and maintain high-quality, performant applications. For complementary testing approaches, explore database performance testing and WebSocket performance testing.