From grafana-app-sdk
Guides k6 performance/load testing with JavaScript/TypeScript scripts: test types (load/stress/spike/soak), thresholds, checks, scenarios, executors, extensions, result analysis, Cloud execution, CI/CD integration.
npx claudepluginhub grafana/skills --plugin grafana-app-sdkThis skill uses the workspace's default tool permissions.
> **Docs**: https://grafana.com/docs/k6/latest/
Guides k6 load testing for HTTP APIs, WebSockets, browsers. Writes scenarios, analyzes results, sets thresholds, integrates with CI/CD.
Generates k6 scripts for performance testing including load testing, stress testing, and benchmarking. Provides step-by-step guidance, best practices, and production-ready code.
Creates and runs load tests with k6, JMeter, and Artillery for web apps and APIs. Validates performance under stress, spike, soak, scalability to detect bottlenecks.
Share bugs, ideas, or general feedback.
# Install
brew install k6 # macOS
sudo apt install k6 # Ubuntu/Debian
choco install k6 # Windows
# Run a test
k6 run script.js
k6 run --vus 10 --duration 30s script.js
k6 run --out json=results.json script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // error rate under 1%
},
};
export default function () {
const res = http.get('https://test.k6.io');
check(res, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 500,
});
sleep(1);
}
export function setup() {
// Runs once before all VUs — return data shared with VUs
const token = getAuthToken();
return { token };
}
export default function (data) {
// Runs for each VU iteration — receives setup() return value
http.get('https://api.example.com', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
export function teardown(data) {
// Runs once after all VUs finish
console.log('Test complete');
}
import http from 'k6/http';
// GET
const res = http.get('https://api.example.com/users');
// POST with JSON body
const payload = JSON.stringify({ name: 'test', value: 42 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post('https://api.example.com/users', payload, params);
// PUT / PATCH / DELETE
http.put(url, body, params);
http.patch(url, body, params);
http.del(url, null, params);
// Batch requests (parallel)
const responses = http.batch([
['GET', 'https://api.example.com/users'],
['GET', 'https://api.example.com/posts'],
]);
// With auth
const res = http.get(url, {
headers: { Authorization: 'Bearer token123' },
});
// Form data
const res = http.post(url, { username: 'user', password: 'pass' });
// File upload
const binFile = open('./image.png', 'b');
const res = http.post(url, {
file: http.file(binFile, 'image.png', 'image/png'),
field: 'value',
});
import { check } from 'k6';
// Checks (soft assertions — don't stop test on failure)
const res = http.get(url);
check(res, {
'status 200': (r) => r.status === 200,
'body contains ID': (r) => r.body.includes('"id"'),
'duration < 500ms': (r) => r.timings.duration < 500,
});
// Thresholds (hard pass/fail criteria)
export const options = {
thresholds: {
// HTTP metrics
http_req_duration: ['p(90)<400', 'p(95)<800', 'avg<200'],
http_req_failed: ['rate<0.05'],
// Custom metric thresholds
my_errors: ['count<10'],
// Tag-based thresholds (specific endpoints)
'http_req_duration{name:homepage}': ['p(95)<500'],
// Abort test if threshold breached
http_req_duration: [{ threshold: 'p(99)<3000', abortOnFail: true }],
},
};
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';
const myErrors = new Counter('my_errors');
const activeUsers = new Gauge('active_users');
const successRate = new Rate('success_rate');
const reqDuration = new Trend('req_duration', true); // true = display in ms
export default function () {
const res = http.get(url);
if (res.status !== 200) myErrors.add(1);
successRate.add(res.status === 200);
reqDuration.add(res.timings.duration);
}
See references/scenarios-executors.md for full reference.
export const options = {
scenarios: {
// Constant VUs
steady_load: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
},
// Ramping VUs (most common)
ramp_up: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
},
// Constant arrival rate (requests per second)
constant_rps: {
executor: 'constant-arrival-rate',
rate: 100, // 100 iterations/sec
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 200,
},
// Per-VU iterations
per_vu: {
executor: 'per-vu-iterations',
vus: 10,
iterations: 100,
},
},
};
See references/test-types.md for full examples.
| Type | Purpose | Load Pattern |
|---|---|---|
| Smoke | Verify script works, minimal load | 1-2 VUs, short duration |
| Load | Normal expected load | Ramp up → steady → ramp down |
| Stress | Beyond normal capacity | Increasing stages until breaking |
| Spike | Sudden traffic surge | Instant spike then drop |
| Soak | Extended duration for memory leaks | Moderate load, hours-long |
| Breakpoint | Find system limits | Continuously increasing load |
# Pass variables to script
k6 run -e MY_VAR=value script.js # access as __ENV.MY_VAR
k6 run --env API_URL=https://api.example.com script.js
# Override options
k6 run --vus 50 --duration 60s script.js
k6 run --iterations 1000 script.js
k6 run --stage 30s:10,1m:50,30s:0 script.js # ramping stages
# Output formats
k6 run --out json=result.json script.js
k6 run --out csv=result.csv script.js
k6 run --out influxdb=http://localhost:8086/k6 script.js
k6 run --out statsd script.js
import { group } from 'k6';
export default function () {
group('homepage', () => {
const res = http.get('/');
check(res, { 'status 200': (r) => r.status === 200 });
});
group('login flow', () => {
group('load login page', () => {
http.get('/login');
});
group('submit credentials', () => {
http.post('/login', { user: 'test', pass: 'test' });
});
});
}
// Custom tags on requests
http.get(url, { tags: { name: 'homepage', version: 'v2' } });
import ws from 'k6/ws';
import { check } from 'k6';
export default function () {
const url = 'ws://echo.websocket.org';
const res = ws.connect(url, {}, function (socket) {
socket.on('open', () => {
socket.send('Hello!');
socket.setInterval(() => socket.ping(), 10000);
});
socket.on('message', (data) => {
check(data, { 'message received': (d) => d.length > 0 });
});
socket.on('error', (e) => console.error(e.error()));
socket.setTimeout(() => socket.close(), 30000);
});
check(res, { 'status 101': (r) => r.status === 101 });
}
import { browser } from 'k6/browser';
import { check } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
options: { browser: { type: 'chromium' } },
},
},
};
export default async function () {
const page = await browser.newPage();
try {
await page.goto('https://test.k6.io/my_messages.php');
await page.locator('input[name="login"]').type('admin');
await page.locator('input[name="password"]').type('123');
await Promise.all([
page.waitForNavigation(),
page.locator('input[type="submit"]').click(),
]);
check(page, { 'header': p => p.locator('h2').textContent() == 'Welcome, admin!' });
} finally {
await page.close();
}
}
// Built-in modules
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';
import { SharedArray } from 'k6/data';
import { scenario, vu } from 'k6/execution';
import ws from 'k6/ws';
import grpc from 'k6/net/grpc';
import { browser } from 'k6/browser';
import crypto from 'k6/crypto';
import encoding from 'k6/encoding';
import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
// jslib utilities
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import { randomItem, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
// Load CSV once, shared across all VUs
const users = new SharedArray('users', function () {
return papaparse.parse(open('./users.csv'), { header: true }).data;
});
// Load JSON
const data = new SharedArray('data', function () {
return JSON.parse(open('./data.json'));
});
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
http.post('/login', { username: user.username, password: user.password });
}
export const options = {
cloud: {
projectID: 123456,
name: 'My Load Test',
note: 'Release candidate',
distribution: {
distributionLabel1: { loadZone: 'amazon:us:ashburn', percent: 50 },
distributionLabel2: { loadZone: 'amazon:eu:dublin', percent: 50 },
},
},
};
# Authenticate
k6 cloud login --token <your-token>
# Run in cloud
k6 cloud script.js
# Run locally but stream results to cloud
k6 run --out cloud script.js
| Flag | Description |
|---|---|
--vus N | Number of virtual users |
--duration Xs/Xm/Xh | Test duration |
--iterations N | Total iterations |
--stage Xm:N | Add ramp stage |
--env KEY=value | Environment variable |
--out FORMAT | Output destination |
--quiet | Suppress progress output |
--no-summary | Skip end-of-test summary |
--summary-export FILE | Export summary as JSON |
--http-debug | Print HTTP request/response details |
--insecure-skip-tls-verify | Skip TLS verification |
--tag KEY=VALUE | Add test-wide tag |
| Metric | Type | Description |
|---|---|---|
http_req_duration | Trend | Total request time |
http_req_failed | Rate | Failed request rate |
http_req_waiting | Trend | Time to first byte (TTFB) |
http_req_connecting | Trend | TCP connection time |
http_req_tls_handshaking | Trend | TLS handshake time |
http_reqs | Counter | Total HTTP requests |
vus | Gauge | Current active VUs |
vus_max | Gauge | Max VUs allocated |
iterations | Counter | Total VU iterations |
iteration_duration | Trend | Time to complete one iteration |
data_sent | Counter | Data sent |
data_received | Counter | Data received |
checks | Rate | Check success rate |