What Is k6?
k6 is a modern, open-source load testing tool built by Grafana Labs. Unlike JMeter’s GUI-driven approach, k6 uses JavaScript scripts that you write in your code editor and run from the command line. This makes it a natural fit for developers and automation engineers who prefer code over configuration.
k6 is written in Go, which gives it excellent performance characteristics. A single machine running k6 can simulate thousands of virtual users with low resource consumption compared to JMeter. The tool integrates naturally into CI/CD pipelines, making it ideal for shift-left performance testing.
k6 vs JMeter: When to Use Which
Before diving into k6, it helps to understand where it shines compared to JMeter.
| Feature | k6 | JMeter |
|---|---|---|
| Scripting | JavaScript (code) | GUI + XML |
| Resource usage | Low (Go runtime) | High (Java/JVM) |
| Protocol support | HTTP, WebSocket, gRPC | HTTP, JDBC, FTP, SMTP, LDAP, JMS |
| CI/CD integration | Native (CLI-first) | Requires plugins |
| Distributed testing | k6 Cloud or xk6-distributed | Built-in master/slave |
| Browser testing | k6 browser module | Not supported |
| Learning curve | Easy if you know JS | Moderate (GUI-based) |
| Community | Growing fast | Very large, mature |
Choose k6 when: Your team knows JavaScript, you need CI/CD integration, you test HTTP/gRPC/WebSocket APIs, or you want lightweight local execution.
Choose JMeter when: You need protocol support beyond HTTP (JDBC, FTP, JMS), your team prefers a GUI, or you need an established ecosystem with extensive plugins.
Installing k6
# macOS
brew install k6
# Windows
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 C5AD17C747E3415A3642D57D77C6C491D6AC1D68
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 run --rm -i grafana/k6 run - <script.js
Verify the installation:
k6 version
Your First k6 Script
Here is a basic k6 script that sends HTTP GET requests:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 10 virtual users
duration: '30s', // run for 30 seconds
};
export default function () {
const res = http.get('https://test-api.k6.io/public/crocodiles/');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains crocodiles': (r) => r.body.includes('crocodiles'),
});
sleep(1); // wait 1 second between iterations
}
Run it:
k6 run script.js
Script Structure
Every k6 script has three key parts:
- Imports: Load k6 modules (
k6/http,k6,k6/metrics) - Options: Configure VUs, duration, stages, thresholds
- Default function: The code each VU executes in a loop
The default function runs once per iteration for each VU. With 10 VUs and a 1-second sleep, you get roughly 10 requests per second.
Virtual Users (VUs) and Iterations
A Virtual User (VU) is a concurrent execution thread. Each VU runs the default function in a loop until the test duration expires or the iteration count is reached.
- VUs: Number of concurrent users
- Iterations: Total number of times the default function executes (across all VUs)
- Duration: How long the test runs
You can specify either iterations or duration, but not both with fixed VU counts.
// Fixed iterations: 100 total iterations shared across 10 VUs
export const options = {
vus: 10,
iterations: 100,
};
// Duration-based: 10 VUs running for 1 minute
export const options = {
vus: 10,
duration: '1m',
};
Stages: Ramp-Up, Steady State, Ramp-Down
For realistic load patterns, use stages to change the VU count over time:
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up to 50 VUs over 2 minutes
{ duration: '5m', target: 50 }, // stay at 50 VUs for 5 minutes
{ duration: '2m', target: 0 }, // ramp down to 0 over 2 minutes
],
};
This creates a classic load test profile: gradual increase, sustained load, and graceful decrease.
Thresholds: Pass/Fail Criteria
Thresholds define success criteria for your test. If any threshold is breached, k6 exits with a non-zero exit code — perfect for CI/CD pipelines.
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // less than 1% failure rate
http_reqs: ['rate>100'], // at least 100 requests/second
checks: ['rate>0.99'], // 99% of checks pass
},
};
Common threshold metrics:
http_req_duration— Response time (p50, p90, p95, p99, avg, max)http_req_failed— Percentage of failed requestshttp_reqs— Request rate (requests per second)checks— Percentage of passed checks
Checks: Inline Assertions
Checks verify response data without stopping the test (unlike assertions in JMeter that mark requests as failed):
check(res, {
'status is 200': (r) => r.status === 200,
'body has expected field': (r) => JSON.parse(r.body).hasOwnProperty('id'),
'response time OK': (r) => r.timings.duration < 300,
});
Checks are recorded as metrics. You can set thresholds on check pass rates.
Working with HTTP Methods
import http from 'k6/http';
// GET
const getRes = http.get('https://api.example.com/users');
// POST with JSON body
const payload = JSON.stringify({ name: 'Test User', email: 'test@example.com' });
const params = { headers: { 'Content-Type': 'application/json' } };
const postRes = http.post('https://api.example.com/users', payload, params);
// PUT
const putRes = http.put('https://api.example.com/users/1', payload, params);
// DELETE
const delRes = http.del('https://api.example.com/users/1');
// Batch requests (parallel)
const responses = http.batch([
['GET', 'https://api.example.com/users'],
['GET', 'https://api.example.com/products'],
['GET', 'https://api.example.com/orders'],
]);
Exercise: Multi-Endpoint Load Test with k6
Write a k6 script that tests an e-commerce API with authentication, multiple endpoints, thresholds, and checks.
Scenario
Test an e-commerce API with the following flow:
- Login to get an auth token
- Browse product catalog
- View a specific product
- Add product to cart
Requirements
- Use stages: ramp to 25 VUs over 1 minute, hold for 3 minutes, ramp down over 1 minute
- Set thresholds: p(95) < 800ms, error rate < 1%, checks pass rate > 99%
- Add checks for status codes and response body content
- Use the auth token in subsequent requests
- Add realistic think time between requests
Hint: Script Structure
import http from 'k6/http';
import { check, sleep, group } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 25 },
{ duration: '3m', target: 25 },
{ duration: '1m', target: 0 },
],
thresholds: {
// define your thresholds here
},
};
export default function () {
// Group 1: Login
// Group 2: Browse catalog
// Group 3: View product
// Group 4: Add to cart
}
Use group() to organize requests into logical sections. This gives you per-group metrics in the results.
Solution: Complete k6 Script
import http from 'k6/http';
import { check, sleep, group } from 'k6';
const BASE_URL = 'https://api.ecommerce.example.com';
export const options = {
stages: [
{ duration: '1m', target: 25 }, // ramp up
{ duration: '3m', target: 25 }, // steady state
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<800'],
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
},
};
export default function () {
let token;
// Step 1: Login
group('Login', function () {
const loginPayload = JSON.stringify({
username: `user${__VU}@example.com`,
password: 'testpass123',
});
const loginRes = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login status is 200': (r) => r.status === 200,
'login returns token': (r) => JSON.parse(r.body).token !== undefined,
});
token = JSON.parse(loginRes.body).token;
});
sleep(1);
const authHeaders = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
};
// Step 2: Browse catalog
let productId;
group('Browse Catalog', function () {
const catalogRes = http.get(`${BASE_URL}/api/products`, authHeaders);
check(catalogRes, {
'catalog status is 200': (r) => r.status === 200,
'catalog returns products': (r) => JSON.parse(r.body).products.length > 0,
});
const products = JSON.parse(catalogRes.body).products;
productId = products[Math.floor(Math.random() * products.length)].id;
});
sleep(2);
// Step 3: View product details
group('View Product', function () {
const productRes = http.get(`${BASE_URL}/api/products/${productId}`, authHeaders);
check(productRes, {
'product status is 200': (r) => r.status === 200,
'product has name': (r) => JSON.parse(r.body).name !== undefined,
'product has price': (r) => JSON.parse(r.body).price > 0,
});
});
sleep(1);
// Step 4: Add to cart
group('Add to Cart', function () {
const cartPayload = JSON.stringify({
productId: productId,
quantity: 1,
});
const cartRes = http.post(`${BASE_URL}/api/cart`, cartPayload, authHeaders);
check(cartRes, {
'cart status is 200 or 201': (r) => r.status === 200 || r.status === 201,
'cart confirms item added': (r) => JSON.parse(r.body).success === true,
});
});
sleep(1);
}
Running the test:
k6 run ecommerce-load-test.js
What to look for in results:
http_req_duration— p95 should be under 800mshttp_req_failed— should be near 0%checks— pass rate should be above 99%http_reqs— total request rate- Per-group metrics show which API endpoint is slowest
Exporting results for analysis:
# JSON output
k6 run --out json=results.json ecommerce-load-test.js
# CSV output
k6 run --out csv=results.csv ecommerce-load-test.js
# InfluxDB (for Grafana dashboards)
k6 run --out influxdb=http://localhost:8086/k6 ecommerce-load-test.js
Pro Tips
- Use
__VUand__ITER: Built-in variables__VU(current VU number) and__ITER(current iteration number) help create unique data per user without external CSV files. - SharedArray for test data: Load large datasets once and share across VUs using
SharedArrayfromk6/data. This prevents each VU from loading its own copy. - Custom metrics: Create custom metrics with
Counter,Gauge,Rate, andTrendfromk6/metricsto track business-specific KPIs. - Scenarios for complex patterns: Use
scenariosinstead ofstageswhen you need multiple executor types (constant-vus, ramping-vus, per-vu-iterations, constant-arrival-rate) running simultaneously. - k6 browser module: For browser-based load testing, k6 now includes a Chromium-based browser module that can measure Core Web Vitals under load.