TL;DR: K6 is a developer-friendly open-source load testing tool written in Go and scriptable in JavaScript. Best for CI/CD integration and modern microservices. Supports HTTP/2, WebSockets, and gRPC. Key metrics: http_req_duration, http_req_failed, vus. K6 is the most adopted open-source load testing tool among cloud-native teams.
K6 is a modern open-source load testing tool designed for developer-friendly performance testing that integrates naturally into CI/CD workflows. Built with Go for high efficiency and scriptable in JavaScript — the language most web developers already know — K6 enables teams to shift performance testing left, running load tests as part of every pull request or deployment pipeline. According to Grafana Labs’ 2024 State of Observability report, K6 has become the most commonly adopted open-source load testing tool among cloud-native engineering teams, with over 90 million Docker pulls. The tool supports HTTP/1.1, HTTP/2, WebSockets, and gRPC protocols, making it suitable for modern microservices architectures. K6 Cloud extends the local tool with distributed cloud execution, real-time result streaming, and baseline comparison, enabling teams to simulate millions of virtual users from multiple geographic regions. Its CLI-first design means test scripts live in version control alongside application code, making performance regression detection as systematic as functional regression testing.
Why K6 for Modern Load Testing?
K6 addresses the limitations of traditional performance testing tools by focusing on developer experience and modern infrastructure:
“K6 changed how we think about performance testing — it became a normal part of the development cycle rather than a special activity before release. When your load tests are JavaScript and live in git, every developer can contribute to performance quality.” — Yuri Kan, Senior QA Lead
- 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.
Official Resources
K6 has achieved widespread adoption among cloud-native teams: according to Grafana Labs’ K6 project page, the tool has surpassed 90 million Docker pulls and is actively maintained as part of the Grafana observability ecosystem.
For production-grade performance testing, the K6 thresholds documentation explains how to define SLA-based pass/fail conditions that automatically fail CI builds when performance degrades.
FAQ
What is K6 and why use it for load testing?
K6 is an open-source load testing tool built with Go and scriptable in JavaScript. It excels at CI/CD integration, supports HTTP/2, WebSockets, and gRPC, and efficiently generates high load from a single machine. Ideal for teams wanting to shift performance testing left into the development workflow.
How does K6 compare to JMeter?
K6 uses JavaScript scripting and is CLI-first — better for CI/CD and developer workflows. JMeter uses a GUI and XML config — better for teams without coding experience. K6 is more resource-efficient for high concurrency; JMeter has broader protocol support and a larger plugin ecosystem.
What are the main K6 metrics to monitor?
Key metrics: http_req_duration (response time percentiles), http_req_failed (error rate), vus (active virtual users), iterations (completed test cycles), data_received/data_sent (bandwidth), http_reqs (request rate). Use thresholds to auto-fail tests exceeding SLA targets.
Can K6 test WebSockets and gRPC?
Yes. K6 supports WebSocket testing via its built-in ws module for real-time applications. For gRPC, K6 provides a grpc module supporting unary calls and streaming. Both integrate with K6 thresholds and metrics for unified load testing of mixed-protocol architectures.
See Also
- API Performance Testing
- Jest & Testing Library: Modern Component Testing for React Applications - Master Jest and Testing Library for React component testing. Learn best…
- Load and performance testing for APIs and services
- Continuous Testing in DevOps - Strategies for integrating testing throughout the development lifecycle
- CI/CD Pipeline Optimization for QA Teams - Best practices for optimizing automated testing pipelines
- API Testing Mastery - Comprehensive guide to API testing strategies
- Containerization for Testing - Using containers for consistent test environments
