Introduction
Overviewk6 is a modern, developer-friendly load testing tool built for testing the performance and reliability of APIs, microservices, and websites. Written in Go, it uses JavaScript (ES6+) for test scripts.
Why k6?
✅ Developer-Friendly
- Write tests in JavaScript/ES6+
- Familiar syntax for developers
- Easy to learn and use
- Great documentation
⚡ High Performance
- Written in Go for efficiency
- Low resource consumption
- Handles thousands of VUs
- Fast execution
🔧 CLI-First
- Easy CI/CD integration
- Scriptable and automatable
- No GUI required
- Version control friendly
☁️ Cloud-Ready
- Grafana Cloud integration
- Multi-region testing
- Advanced analytics
- Team collaboration
Key Features
- Protocol Support: HTTP/1.1, HTTP/2, WebSockets, gRPC
- Browser Testing: Real browser automation with Chromium
- Custom Metrics: Track business-specific KPIs
- Thresholds: Define pass/fail criteria
- Scenarios: Complex test orchestration
- Extensions: Extend functionality with Go
Installation
SetupWindows Installation
# Option 1: Using Chocolatey
choco install k6
# Option 2: Using Winget
winget install k6 --source winget
# Option 3: Using Scoop
scoop install k6
macOS Installation
# Using Homebrew
brew install k6
Linux Installation
# 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
# Fedora/CentOS
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6
Verify Installation
k6 version
k6 v0.50.0 (go1.21.5, windows/amd64)
Core Concepts
FundamentalsVirtual Users (VUs)
Virtual Users simulate concurrent users interacting with your system. Each VU runs your test script independently in a loop.
export const options = {
vus: 10, // 10 concurrent users
duration: '30s' // for 30 seconds
};
Stages
Stages allow you to ramp load up and down, simulating realistic traffic patterns.
export const options = {
stages: [
{ duration: '30s', target: 10 }, // Ramp up to 10 VUs
{ duration: '1m', target: 10 }, // Stay at 10 VUs
{ duration: '30s', target: 0 } // Ramp down to 0
]
};
Thresholds
Thresholds define pass/fail criteria for your tests. If any threshold fails, k6 exits with a non-zero status code.
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests < 500ms
http_req_failed: ['rate<0.1'], // Less than 10% failures
checks: ['rate>0.9'] // More than 90% checks pass
}
};
Checks
Checks are like assertions - they validate responses but don't stop execution on failure.
import { check } from 'k6';
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains text': (r) => r.body.includes('success')
});
- Checks: Validate individual responses, don't fail the test
- Thresholds: Aggregate metrics, fail the test if not met
Metrics Deep Dive
MetricsBuilt-in HTTP Metrics
k6 automatically collects these metrics for every HTTP request:
| Metric | Description | Type |
|---|---|---|
http_req_duration | Total request time | Trend |
http_req_blocked | Time waiting for TCP connection | Trend |
http_req_connecting | Time establishing TCP connection | Trend |
http_req_tls_handshaking | Time for TLS handshake | Trend |
http_req_sending | Time sending request | Trend |
http_req_waiting | Time waiting for response (TTFB) | Trend |
http_req_receiving | Time receiving response | Trend |
http_req_failed | Rate of failed requests | Rate |
http_reqs | Total number of requests | Counter |
HTTP Timing Breakdown
|--blocked--|--connecting--|--tls_handshaking--|--sending--|--waiting--|--receiving--|
↑
Server processing time (TTFB)
Custom Metrics
k6 provides 4 custom metric types for tracking business-specific KPIs:
1. Counter - Cumulative Sum
Tracks a running total (only increases).
import { Counter } from 'k6/metrics';
const errorCount = new Counter('custom_errors');
export default function() {
errorCount.add(1); // Increment by 1
}
Use for: Total errors, successful operations, items processed
2. Gauge - Latest Value
Tracks the most recent value (like a speedometer).
import { Gauge } from 'k6/metrics';
const activeConnections = new Gauge('active_connections');
export default function() {
activeConnections.add(5); // Now shows 5
}
Use for: Current active users, memory usage, queue size
3. Trend - Statistical Analysis
Tracks multiple values and calculates statistics (min, max, avg, percentiles).
import { Trend } from 'k6/metrics';
const responseTime = new Trend('custom_response_time');
export default function() {
const res = http.get('https://api.example.com');
responseTime.add(res.timings.waiting); // Track server processing time
}
Use for: Response times, processing durations, file sizes
4. Rate - Success/Failure Ratio
Tracks percentage of true vs false values.
import { Rate } from 'k6/metrics';
const successRate = new Rate('success_rate');
export default function() {
const res = http.get('https://api.example.com');
successRate.add(res.status === 200); // true or false
}
Use for: Success rates, error rates, pass/fail ratios
Test Types
Test TypesDifferent test types serve different purposes. Choose the right test type for your goals.
1. Smoke Test 🔥
Purpose: Verify system works under minimal load
Configuration
- VUs: 1-2
- Duration: 30s - 1m
- Goal: Catch basic errors
When to Use
- Before every deployment
- As a sanity check
- Before larger tests
export const options = {
vus: 1,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01']
}
};
2. Load Test 📈
Purpose: Test system under expected normal traffic
Configuration
- VUs: 10-100
- Duration: 5-15 minutes
- Goal: Validate typical performance
When to Use
- Regular performance validation
- Baseline establishment
- SLA verification
export const options = {
stages: [
{ duration: '2m', target: 10 }, // Ramp up
{ duration: '5m', target: 10 }, // Stay at load
{ duration: '2m', target: 0 } // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<400'],
http_req_failed: ['rate<0.05']
}
};
3. Stress Test 💪
Purpose: Find the breaking point of your system
Configuration
- VUs: 100-500+
- Duration: 10-20 minutes
- Goal: Find maximum capacity
When to Use
- Capacity planning
- Finding bottlenecks
- Failure mode testing
export const options = {
stages: [
{ duration: '2m', target: 10 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 }
]
};
4. Spike Test ⚡
Purpose: Test system resilience to sudden traffic surges
Configuration
- VUs: Sudden jump (5 → 100)
- Duration: 5-10 minutes
- Goal: Validate auto-scaling
When to Use
- Testing auto-scaling
- Flash sale scenarios
- Sudden traffic spikes
export const options = {
stages: [
{ duration: '10s', target: 5 },
{ duration: '1m', target: 100 }, // Sudden spike!
{ duration: '10s', target: 5 },
{ duration: '3m', target: 5 },
{ duration: '10s', target: 0 }
]
};
5. Soak Test ⏱️
Purpose: Find memory leaks and degradation over time
Configuration
- VUs: Moderate (20-50)
- Duration: 1-24 hours
- Goal: Identify resource leaks
When to Use
- Before major releases
- Stability validation
- Memory leak detection
export const options = {
stages: [
{ duration: '2m', target: 20 },
{ duration: '3h', target: 20 }, // Extended duration
{ duration: '2m', target: 0 }
]
};
Writing Your First Test
TutorialBasic HTTP GET Test
import { sleep } from 'k6';
import http from 'k6/http';
export const options = {
vus: 3,
duration: '10s',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.1']
}
};
export default function() {
http.get('https://test.k6.io');
sleep(1); // Think time between requests
}
Run the Test
k6 run test.js
Understanding the Output
scenarios: (100.00%) 1 scenario, 3 max VUs, 40s max duration
default: 3 looping VUs for 10s
✓ http_req_duration..............: avg=225ms min=216ms med=229ms max=231ms p(95)=231ms
✓ http_req_failed................: 0.00% ✓ 0 ✗ 24
http_reqs......................: 24 2.31/s
iteration_duration.............: avg=1.29s min=1.21s med=1.22s max=1.76s
iterations.....................: 24 2.31/s
vus............................: 3 min=3 max=3
- ✓ = Threshold passed
- ✗ = Threshold failed
- p(95) = 95th percentile (95% of requests were faster)
- http_reqs = Total requests made
- iterations = How many times the default function ran
POST Request with JSON
import http from 'k6/http';
import { check } from 'k6';
export default function() {
const url = 'https://httpbin.test.k6.io/post';
const payload = JSON.stringify({
username: 'testuser',
password: 'password123'
});
const params = {
headers: {
'Content-Type': 'application/json'
}
};
const res = http.post(url, payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
'response has json': (r) => r.json('json') !== undefined
});
}
Advanced Features
AdvancedGroups
Organize related requests and get separate metrics for each group.
import { group } from 'k6';
import http from 'k6/http';
export default function() {
group('User Login', () => {
http.post('https://api.example.com/login', payload);
});
group('Browse Products', () => {
http.get('https://api.example.com/products');
});
group('Add to Cart', () => {
http.post('https://api.example.com/cart', item);
});
}
Tags
Label requests for filtering and analysis.
http.get('https://api.example.com/users', {
tags: { name: 'api', endpoint: 'users' }
});
// Set thresholds for tagged requests
export const options = {
thresholds: {
'http_req_duration{name:api}': ['p(95)<500']
}
};
Data Parameterization
Load test data from external files.
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() {
const user = csvData[Math.floor(Math.random() * csvData.length)];
const payload = JSON.stringify({
username: user.username,
password: user.password
});
http.post('https://api.example.com/login', payload);
}
Scenarios
Run multiple test scenarios simultaneously with different configurations.
export const options = {
scenarios: {
smoke_test: {
executor: 'constant-vus',
exec: 'smokeTest',
vus: 1,
duration: '30s'
},
load_test: {
executor: 'ramping-vus',
exec: 'loadTest',
startVUs: 0,
stages: [
{ duration: '2m', target: 10 },
{ duration: '5m', target: 10 },
{ duration: '2m', target: 0 }
],
startTime: '30s' // Start after smoke test
}
}
};
export function smokeTest() {
http.get('https://api.example.com/health');
}
export function loadTest() {
http.get('https://api.example.com/products');
}
Executors
Control how VUs and iterations are scheduled.
| Executor | Description | Use Case |
|---|---|---|
shared-iterations | Fixed total iterations shared among VUs | Exact number of test runs |
per-vu-iterations | Each VU runs fixed iterations | Guaranteed iterations per VU |
constant-vus | Fixed number of VUs for duration | Steady load |
ramping-vus | Gradually increase/decrease VUs | Load/stress tests |
constant-arrival-rate | Fixed request rate | Throughput testing |
ramping-arrival-rate | Gradually change request rate | Gradual load increase |
Dynamic Data Handling
Extract and reuse dynamic values like tokens and IDs.
export default function() {
// Login and get token
const loginRes = http.post('https://api.example.com/login', payload);
const token = loginRes.json('token');
// Use token in subsequent requests
const params = {
headers: {
'Authorization': `Bearer ${token}`
}
};
const profileRes = http.get('https://api.example.com/profile', params);
check(profileRes, {
'authenticated': (r) => r.status === 200
});
}
Environment Variables
Keep sensitive data out of scripts.
const BASE_URL = __ENV.BASE_URL || 'https://api.example.com';
const API_KEY = __ENV.API_KEY;
export default function() {
http.get(`${BASE_URL}/data`, {
headers: { 'X-API-Key': API_KEY }
});
}
# Run with environment variables
k6 run -e BASE_URL=https://staging.api.com -e API_KEY=secret test.js
Browser Testing
Browserk6 supports browser automation for testing real user interactions using Chromium.
Basic Browser Test
import { browser } from 'k6/browser';
import { check } from 'k6';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: {
browser: {
type: 'chromium',
headless: true // Set to false for debugging
}
}
}
}
};
export default async function() {
const page = await browser.newPage();
try {
await page.goto('https://example.com/login');
await page.locator('#username').fill('testuser');
await page.locator('#password').fill('password123');
await page.locator('button[type="submit"]').click();
await page.waitForSelector('.success-message');
const successText = await page.locator('.success-message').textContent();
check(successText, {
'login successful': (text) => text.includes('Welcome')
});
} finally {
await page.close();
}
}
Browser Selectors
// CSS Selector
await page.locator('.login-button').click();
// XPath
await page.locator("//button[text()='Login']").click();
// Playwright-style
await page.getByRole('button', { name: 'Login' }).click();
await page.getByPlaceholder('Username').fill('user');
await page.getByText('Welcome').isVisible();
Combining Browser + API Tests
Test both frontend and backend simultaneously.
import { browser } from 'k6/browser';
import http from 'k6/http';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
exec: 'browserTest',
vus: 2,
iterations: 4
},
api: {
executor: 'constant-vus',
exec: 'apiTest',
vus: 10,
duration: '1m'
}
}
};
export async function browserTest() {
const page = await browser.newPage();
await page.goto('https://example.com');
await page.close();
}
export function apiTest() {
http.get('https://api.example.com/products');
}
- Use
headless: truefor CI/CD environments - Browser tests consume more resources than API tests
- Limit concurrent browser VUs (2-5 typically)
- Always close pages to free resources
Cloud Integration
CloudGrafana Cloud k6 allows you to run tests from multiple geographic locations and get detailed analytics.
Setup
- Create account at app.k6.io
- Get your API token from the dashboard
- Login via CLI:
k6 cloud login - Enter your stack name and paste your token
Cloud Configuration
export const options = {
cloud: {
projectID: 1234567,
name: 'My Performance Test',
distribution: {
'amazon:us:ashburn': { loadZone: 'amazon:us:ashburn', percent: 50 },
'amazon:eu:dublin': { loadZone: 'amazon:eu:dublin', percent: 50 }
}
},
stages: [
{ duration: '5m', target: 100 }
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01']
}
};
Running Cloud Tests
# Run test in cloud
k6 cloud test.js
# Run locally and stream results to cloud
k6 run --out cloud test.js
Available Load Zones
| Region | Load Zone |
|---|---|
| US East (Virginia) | amazon:us:ashburn |
| US West (Oregon) | amazon:us:portland |
| Europe (Ireland) | amazon:ie:dublin |
| Europe (Frankfurt) | amazon:de:frankfurt |
| Asia Pacific (Tokyo) | amazon:jp:tokyo |
| Asia Pacific (Singapore) | amazon:sg:singapore |
| Asia Pacific (Sydney) | amazon:au:sydney |
| South America (São Paulo) | amazon:br:sao-paulo |
Cloud Benefits
🌍 Multi-Region Testing
- Test from 15+ locations worldwide
- Simulate global user base
- Identify regional issues
📊 Advanced Analytics
- Detailed performance insights
- Visual dashboards
- Historical comparisons
👥 Team Collaboration
- Share results with team
- Centralized test management
- Role-based access control
🔄 CI/CD Integration
- Automated performance testing
- Trend analysis over time
- Performance regression detection
Best Practices
Best Practices1. Start Small
Always run a smoke test before larger tests to catch basic errors.
k6 run --vus 1 --duration 30s test.js
2. Use Realistic Think Time
Add sleep() to simulate real user behavior.
import { sleep } from 'k6';
export default function() {
http.get('https://api.example.com/products');
sleep(Math.random() * 3 + 1); // 1-4 seconds
http.get('https://api.example.com/cart');
sleep(2);
}
3. Set Meaningful Thresholds
Base thresholds on business requirements and SLAs.
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
'http_req_duration{name:api}': ['p(99)<1000'] // Critical API
}
4. Use Groups for Organization
group('User Journey', () => {
group('Login', () => { /* ... */ });
group('Browse', () => { /* ... */ });
group('Checkout', () => { /* ... */ });
});
5. Monitor Custom Metrics
Track business-specific metrics.
import { Counter, Rate } from 'k6/metrics';
const successfulOrders = new Counter('successful_orders');
const authRate = new Rate('authentication_rate');
export const options = {
thresholds: {
successful_orders: ['count>100'],
authentication_rate: ['rate>0.95']
}
};
6. Validate Responses
Always check responses, not just status codes.
check(response, {
'status is 200': (r) => r.status === 200,
'response has data': (r) => r.json('data') !== undefined,
'data is not empty': (r) => r.json('data').length > 0,
'response time OK': (r) => r.timings.duration < 500
});
7. Ramp Load Gradually
Avoid shocking your system with sudden load.
stages: [
{ duration: '2m', target: 10 }, // Gradual ramp
{ duration: '5m', target: 10 }, // Sustained load
{ duration: '2m', target: 0 } // Cool down
]
8. Test in Non-Production First
Always test in staging/QA environments before production testing.
✅ DO
- Start with smoke tests
- Use realistic think times
- Set meaningful thresholds
- Validate response content
- Ramp load gradually
- Monitor system resources
- Test in staging first
- Use environment variables
❌ DON'T
- Skip smoke tests
- Use zero think time
- Ignore response validation
- Spike load immediately
- Test only status codes
- Hardcode credentials
- Test production directly
- Ignore error messages
CI/CD Integration
CI/CDIntegrate k6 performance tests into your CI/CD pipeline for automated performance validation.
GitHub Actions
name: k6 Performance Tests
on:
push:
branches: [main, develop]
pull_request:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
performance-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run k6 smoke test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/smoke.js
- name: Run k6 load test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/load.js
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: k6-results
path: |
summary.json
results.json
retention-days: 7
GitLab CI
stages:
- test
k6-performance:
stage: test
image: grafana/k6:latest
script:
- k6 run --out json=results.json tests/load.js
artifacts:
paths:
- results.json
expire_in: 1 week
only:
- main
- develop
Azure Pipelines
trigger:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
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
displayName: 'Install k6'
- script: k6 run tests/load.js
displayName: 'Run k6 tests'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/results.xml'
Docker Integration
# Run k6 in Docker
docker run --rm -v $(pwd):/tests grafana/k6:latest run /tests/load.js
# With environment variables
docker run --rm \
-e K6_CLOUD_TOKEN=$K6_CLOUD_TOKEN \
-v $(pwd):/tests \
grafana/k6:latest cloud /tests/load.js
Fail Pipeline on Threshold Violations
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01']
}
};
// k6 exits with code 99 if thresholds fail
// CI/CD pipeline will fail automatically
Export Results for Analysis
# Export to JSON
k6 run --out json=results.json test.js
# Export to CSV
k6 run --out csv=results.csv test.js
# Export to InfluxDB
k6 run --out influxdb=http://localhost:8086/k6 test.js
# Multiple outputs
k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 test.js
Performance Trend Tracking
# GitHub Actions - Track performance over time
- name: Run k6 and export results
run: |
k6 run --out json=results.json tests/load.js
- name: Parse results
run: |
P95=$(jq '.metrics.http_req_duration.values["p(95)"]' results.json)
echo "P95_LATENCY=$P95" >> $GITHUB_ENV
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Performance Test Results\n\n- P95 Latency: ${process.env.P95_LATENCY}ms`
})
Quick Reference
ReferenceCommon Commands
| Command | Description |
|---|---|
k6 run test.js | Run a test |
k6 run --vus 10 --duration 30s test.js | Run with custom VUs and duration |
k6 run -e BASE_URL=https://api.com test.js | Run with environment variables |
k6 cloud test.js | Run in Grafana Cloud |
k6 run --out json=results.json test.js | Export results to JSON |
k6 run --verbose test.js | Run with verbose output |
k6 inspect test.js | Validate test script |
k6 version | Show k6 version |
Percentile Guide
| Percentile | Meaning |
|---|---|
p(50) | Median - 50% of requests were faster |
p(90) | 90% of requests were faster |
p(95) | 95% of requests were faster (common SLA) |
p(99) | 99% of requests were faster |
p(99.9) | 99.9% of requests were faster (tail latency) |
Threshold Operators
thresholds: {
http_req_duration: ['p(95)<500'], // Less than
http_req_failed: ['rate<0.01'], // Less than 1%
checks: ['rate>0.95'], // Greater than 95%
http_reqs: ['count>1000'], // More than 1000 requests
'http_req_duration{name:api}': ['avg<200'] // Average less than 200ms
}
Test Script Template
import { check, group, sleep } from 'k6';
import http from 'k6/http';
import { Rate, Counter, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const successfulRequests = new Counter('successful_requests');
const customDuration = new Trend('custom_duration');
// Test configuration
export const options = {
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 10 },
{ duration: '1m', target: 0 }
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
errors: ['rate<0.1']
}
};
// Setup function (runs once)
export function setup() {
// Prepare test data
return { token: 'test-token' };
}
// Main test function
export default function(data) {
group('API Tests', () => {
const res = http.get('https://api.example.com/data');
const success = check(res, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 500
});
if (success) {
successfulRequests.add(1);
} else {
errorRate.add(1);
}
customDuration.add(res.timings.duration);
});
sleep(1);
}
// Teardown function (runs once)
export function teardown(data) {
// Cleanup
}