skills/k6-load-testing/SKILL.md
Creates and runs Grafana k6 load testing scripts for traffic simulation, performance testing, and stress testing. Use this skill whenever the user wants to load test an API, simulate traffic against a service, write a k6 script, do performance testing, stress testing, spike testing, soak testing, smoke testing, breakpoint testing, or any kind of traffic simulation. Also use when the user mentions k6, VUs (virtual users), ramping load, arrival rate, or wants to benchmark an endpoint's response time under load. Even if the user just says "test my API under load" or "how many requests can my server handle" — this skill is the right choice.
npx skillsauth add alahmadiq8/skills k6-load-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert at writing and running Grafana k6 load testing scripts. k6 is an open-source,
developer-friendly load testing tool that uses JavaScript for scripting and runs from the CLI
via k6 run script.js.
Before writing any script, confirm:
If the user is vague, default to a smoke test first (3 VUs, 1 minute) so they can validate the script works, then suggest scaling up.
Every k6 script follows this lifecycle:
// 1. Init — runs once per VU, before the test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
// test configuration goes here
};
// 2. (Optional) Setup — runs once before all VUs
export function setup() {
// e.g., authenticate and return a token
}
// 3. VU code — runs repeatedly for each VU
export default function (data) {
// the actual requests to test
}
// 4. (Optional) Teardown — runs once after all VUs finish
export function teardown(data) {
// cleanup
}
Key rules:
import statements and file reads (open()) work only in init contextsetup(), and teardown() — but not in init contextsetup() is passed as an argument to default and teardownChoose the right test type based on what the user wants to learn. Read references/test-types.md
for full details on each type with ready-to-use templates.
| Type | Purpose | VUs | Duration | |------|---------|-----|----------| | Smoke | Validate script works, baseline metrics | 1–5 | 30s–3m | | Average-load | Normal day traffic | Production average | 5–60m | | Stress | Above-average load | 1.5–2× average | 5–60m | | Spike | Sudden massive traffic | Very high | 1–5m | | Soak | Extended reliability | Average | Hours | | Breakpoint | Find system limits | Ramps until break | Until failure |
k6 has two models for generating load:
constant-vus — fixed VU count for a durationramping-vus — ramp VUs up/down through stagesshared-iterations — fixed total iterations split across VUsper-vu-iterations — each VU runs N iterationsconstant-arrival-rate — fixed iteration rate (e.g., 50 req/s)ramping-arrival-rate — ramp iteration rate through stagesWhen to use which: If the user talks about "concurrent users", use VU-based. If they talk
about "requests per second" or "throughput", use arrival-rate. For breakpoint tests, prefer
ramping-arrival-rate because it keeps increasing load regardless of response time.
Read references/executors.md for configuration details and examples.
import http from 'k6/http';
// GET
const res = http.get('https://api.example.com/users');
// POST with JSON
const payload = JSON.stringify({ name: 'test', email: '[email protected]' });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post('https://api.example.com/users', payload, params);
// All methods: get, post, put, patch, del, head, options, request
// Bearer token
const params = {
headers: { Authorization: `Bearer ${token}` },
};
// Basic auth (in setup, pass token to VU code via return value)
export function setup() {
const loginRes = http.post('https://api.example.com/login', JSON.stringify({
username: 'user', password: 'pass'
}), { headers: { 'Content-Type': 'application/json' } });
return { token: loginRes.json('token') };
}
export default function (data) {
const res = http.get('https://api.example.com/protected', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
When testing endpoints with dynamic IDs, group them so metrics aren't fragmented:
// Option 1: Use http.url tagged template
http.get(http.url`https://api.example.com/users/${userId}`);
// Option 2: Use name tag
http.get(`https://api.example.com/users/${userId}`, {
tags: { name: 'GetUser' },
});
Checks validate individual responses (don't abort on failure):
import { check } from 'k6';
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains expected data': (r) => r.body.includes('success'),
});
Thresholds define pass/fail criteria for the entire test:
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // <1% errors
http_req_duration: ['p(95)<500'], // 95th percentile < 500ms
http_req_duration: ['p(95)<500', 'p(99)<1000'], // multiple on same metric
'http_req_duration{name:GetUser}': ['p(95)<300'], // per-endpoint
checks: ['rate>0.95'], // 95% of checks must pass
},
};
Threshold expressions use: avg, min, max, med, p(N) for Trends; rate for Rates; count/rate for Counters; value for Gauges.
| Metric | Type | What it measures |
|--------|------|------------------|
| http_reqs | Counter | Total HTTP requests |
| http_req_duration | Trend | Total request time (send + wait + receive) |
| http_req_failed | Rate | Ratio of failed requests |
| http_req_waiting | Trend | Time to first byte (TTFB) |
| iterations | Counter | Completed VU iterations |
| iteration_duration | Trend | Time per full iteration |
| checks | Rate | Ratio of successful checks |
| data_received / data_sent | Counter | Network data volume |
import { Trend, Counter, Rate, Gauge } from 'k6/metrics';
const apiDuration = new Trend('api_duration');
const errorCount = new Counter('errors');
const successRate = new Rate('success_rate');
export default function () {
const res = http.get('https://api.example.com/data');
apiDuration.add(res.timings.duration);
errorCount.add(res.status !== 200 ? 1 : 0);
successRate.add(res.status === 200);
}
Scenarios let you define multiple independent workloads in a single script:
export const options = {
scenarios: {
browse: {
executor: 'constant-vus',
vus: 10,
duration: '5m',
exec: 'browseProducts',
},
purchase: {
executor: 'ramping-arrival-rate',
startRate: 1,
timeUnit: '1s',
preAllocatedVUs: 20,
stages: [
{ duration: '2m', target: 10 },
{ duration: '3m', target: 10 },
{ duration: '1m', target: 0 },
],
exec: 'makePurchase',
},
},
};
export function browseProducts() { /* ... */ }
export function makePurchase() { /* ... */ }
Make scripts reusable across environments:
const BASE_URL = __ENV.BASE_URL || 'https://api.staging.example.com';
export default function () {
http.get(`${BASE_URL}/users`);
}
Run with: k6 run -e BASE_URL=https://api.prod.example.com script.js
Use groups to organize requests into logical transactions:
import { group } from 'k6';
export default function () {
group('user_login', function () {
http.post(/*...*/);
});
group('browse_products', function () {
http.get(/*...*/);
http.get(/*...*/);
});
}
Use tags to filter metrics:
http.get(url, { tags: { type: 'API' } });
// Then set thresholds on tagged sub-metrics
export const options = {
thresholds: {
'http_req_duration{type:API}': ['p(95)<300'],
},
};
k6 supports more than HTTP:
import { WebSocket } from 'k6/websockets'; — for real-time connectionsimport grpc from 'k6/net/grpc'; — for gRPC servicesreferences/protocols.md for WebSocket and gRPC examples# Basic run
k6 run script.js
# With options
k6 run --vus 10 --duration 30s script.js
# With environment variables
k6 run -e BASE_URL=https://api.example.com script.js
# Output to JSON
k6 run --out json=results.json script.js
# Output to CSV
k6 run --out csv=results.csv script.js
sleep() between requests in VU-based executors to simulate realistic think time.
Don't use sleep in arrival-rate executors (they already pace iterations).discardResponseBodies: true if you don't need response bodies — reduces memorycheck() + thresholds together — checks alone don't fail the testhttp.url or name tags to prevent metric explosionsetup() for auth — authenticate once, share the token across VUsSharedArray for large test data — shares memory across VUs:
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json'));
});
For detailed information, consult these reference files:
references/test-types.md — Complete templates for each test typereferences/executors.md — All executor configurations with examplesreferences/protocols.md — WebSocket and gRPC usagereferences/options-reference.md — Full list of k6 optionsSource documentation: https://grafana.com/docs/k6/latest/
development
Generate reveal.js HTML slide presentations from topics, outlines, or content. Use when the user asks to create slides, presentations, decks, or talks. Triggers on: "create a presentation", "make slides about", "build a deck", "generate a talk", "slide deck for", or any request involving presentation creation. Supports code highlighting, fragments, speaker notes, math, markdown, and Mermaid.js diagrams (flowcharts, sequence, class, ER, mindmap, timeline, etc.).
tools
Edit and manage Obsidian vaults — create, read, update, and delete notes, manage properties/frontmatter, handle links and backlinks, work with tags, tasks, daily notes, templates, bases, and more. Uses the Obsidian CLI for safe vault operations when available, with direct file editing as fallback. Use this skill whenever the user mentions Obsidian, vault, knowledge base notes with wikilinks, frontmatter/properties on markdown files, daily notes, or any task involving an Obsidian vault — even if they just say "my notes" or reference a folder that looks like a vault (has .obsidian/ directory).
testing
Write Obsidian Dataview queries (DQL, inline DQL, and DataviewJS) from natural language descriptions. Use this skill whenever the user wants to query, filter, list, table, or summarize their Obsidian notes using Dataview — even if they just describe what data they want to see without mentioning "dataview" explicitly. Trigger on phrases like "show me all notes tagged...", "list my tasks due...", "table of books by rating", "query my vault for...", "create a dataview query", "dataview", "DQL", or any request to dynamically display, filter, sort, or aggregate note metadata in Obsidian.
development
Build, configure, and develop Hugo static sites and themes. Use when the user wants to create a new Hugo site, develop or customize a Hugo theme, write Hugo templates (layouts, partials, shortcodes), configure hugo.toml/yaml/json, work with Hugo's asset pipeline (images, CSS/Sass, JS bundling), manage content (pages, sections, taxonomies, menus), or deploy a Hugo site. Triggers on mentions of "Hugo", "hugo.toml", "static site generator", Hugo-related template syntax (Go templates, baseof, partials), or Hugo content workflows.