Complete Guide — Setup & Commands
From zero to production-ready load testing: install k6, run smoke gates against qaschool/testflow, tune SLO thresholds, publish HTML reports, and wire Grafana.
Smoke · Load · Stress · Spike · Soak · Journey · BrowserWhat is k6?
Grafana k6 is an open-source load testing tool for API, microservices, and browser performance. Scripts are written in JavaScript (ES modules), executed by the k6 runtime with a Go-based engine that generates real HTTP traffic at scale.
Developer-first
Version-controlled JS scripts, npm scripts, and CI gates — same workflow as Cypress and Playwright.
Built-in metrics
http_req_duration, http_req_failed, checks, VUs, and custom Trend/Rate/Counter.
SLO thresholds
Fail a run when latency, error rate, or check pass rate breaches limits — not just when the app crashes.
Extensible output
JSON summary, HTML dashboard, InfluxDB streaming, Grafana Cloud, and k6 web dashboard export.
Why use k6 in testflow-k6?
The testflow-k6
suite complements functional automation in testflow-cypress, testflow-playwright, and
testflow-pytest. It validates Service Level Objectives (SLOs) for the
TestFlow sandbox on port
5050 under smoke, load, stress, spike, and soak patterns.
- Same endpoints —
/health,/api/auth/login,/api/users, static/web/*.html - Aligned SLO budgets — per-endpoint p95 thresholds mirror functional test timeouts
- CI smoke gate — every push runs a lightweight profile against Docker TestFlow
- GitHub Pages report — 7-scenario HTML dashboard updated on every push to
main - Shared credentials —
demo@automation.io/DEMO_PASSWORDenv var
test:browser:login when you need Chromium under load; use API scenarios for capacity testing.
Prerequisites
Before cloning the repository, ensure your machine meets these requirements.
Node.js 20+
Required for npm scripts, report generation, and local docs server.
TestFlow on 5050
Target app: qaschool/testflow:latest — health at http://localhost:5050/health.
k6 v1.0+
Native binary recommended. Docker fallback via scripts/run-k6.sh when k6 is not installed.
Docker (optional)
Run TestFlow, k6-influx image, Grafana + InfluxDB stack, or full compose profile.
Project setup
git clone https://github.com/lflucasferreira/testflow-k6.git
cd testflow-k6
npm install
cp .env.example .env # optional — BASE_URL, DEMO_PASSWORD, InfluxDB tokens
Environment setup by platform
Install Node.js 20+, Git, and Docker (for TestFlow on port
5050). Project steps (npm install, k6 runs) are identical on every OS.
macOS
# Homebrew — https://brew.sh
brew install node@20 git
brew link --overwrite node@20
brew install --cask docker
node --version # v20+
git --version
Homebrew · Docker Desktop
Windows
winget install OpenJS.NodeJS.LTS
winget install Git.Git
winget install Docker.DockerDesktop
node --version
winget · PowerShell · Git Bash
Linux (Debian / Ubuntu)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git docker.io
sudo usermod -aG docker $USER
node --version
NodeSource · Docker · log out after usermod for group membership.
Start TestFlow — same on every platform
docker run --rm -p 5050:5050 qaschool/testflow:latest
# or
npm run docker:up
k6 installation
Native k6 is faster for local development and required for browser scenarios
(test:browser:login). If k6 is missing, npm scripts fall back to the
grafana/k6 Docker image via scripts/run-k6.sh.
macOS — Homebrew
brew install k6
k6 version
Linux — apt (Debian/Ubuntu)
curl -fsSL https://dl.k6.io/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/k6-archive-keyring.gpg
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 -y k6
k6 version
Windows — winget
winget install k6 --source winget
k6 version
Verify installation
brew install k6 or apt fails with
certificate errors, use the Docker fallback (npm run docker:smoke) or configure system CA
certificates — same approach as testflow-playwright.
Project structure
testflow-k6/
├── config/ # environments.js, profiles.js, thresholds.js
├── lib/ # auth.js, http.js, endpoints.js, summary.js
├── scenarios/
│ ├── smoke/ # CI gate — api-health.js
│ ├── load/ # api-auth, api-users, mixed-traffic
│ ├── stress/ # api-breakpoint.js
│ ├── spike/ # api-spike.js
│ ├── soak/ # api-endurance.js
│ └── browser/ # login-page.js (k6 browser)
├── journeys/ # login-flow.js, authenticated-user.js
├── docs/ # GitHub Pages hub, slides, this guide
├── monitoring/ # Grafana + InfluxDB provisioning
│ ├── grafana/ # datasource + k6 dashboard JSON
│ └── influxdb/ # YAML template + dashboard JSON
├── scripts/ # run-k6.sh, generate-report.mjs, grafana-up.sh
├── results/ # summary JSON, HTML report, REPORT.md
├── docker/ # k6-influx.Dockerfile (xk6-output-influxdb)
├── docker-compose.yml
└── .github/workflows/
└── k6.yml # smoke gate, manual load, GitHub Pages
config/
environments.js — BASE_URL, credentials. profiles.js — VU stages. thresholds.js — SLO gates.
lib/
Reusable HTTP helpers with built-in check() assertions. Never duplicate endpoint logic in scenarios.
scenarios/
Single-purpose load tests grouped by type: smoke, load, stress, spike, soak, browser.
journeys/
Multi-step user flows using group() for step-level timing in reports.
monitoring/
Grafana dashboards and InfluxDB templates for local stack and cloud streaming.
docs/
Landing page, Reveal.js slides, EN/PT walkthroughs, threshold strategy, complete guides.
First scenario — smoke api-health.js
The smoke gate is the fastest way to validate that TestFlow responds under minimal load. Run it with
npm run test:smoke.
-
Start TestFlow
docker run --rm -p 5050:5050 qaschool/testflow:latest -
Run the smoke scenario
npm run test:smoke -
Inspect the script — file:
scenarios/smoke/api-health.js
import { sleep } from 'k6';
import { getProfile } from '../../config/profiles.js';
import { getThresholds } from '../../config/thresholds.js';
import { TAGS } from '../../config/environments.js';
import { login } from '../../lib/auth.js';
import { getHealth, getUsers } from '../../lib/http.js';
import { handleSummary } from '../../lib/summary.js';
export { handleSummary };
const profile = getProfile('smoke');
export const options = {
scenarios: {
smoke_api: {
executor: 'ramping-vus',
...profile,
tags: { test_type: TAGS.smoke, suite: 'api-health' },
},
},
thresholds: getThresholds('smoke'),
};
export default function smokeApiHealth() {
getHealth();
sleep(0.5);
getUsers();
sleep(0.5);
login();
sleep(1);
}
Each iteration exercises three critical endpoints:
GET /health— availability probeGET /api/users— read-heavy list with JSON validationPOST /api/auth/login— authentication and token extraction
Walkthrough: docs/en/scenarios/smoke/api-health.md
Load profiles
Profiles in config/profiles.js define stages for the
ramping-vus executor. Select a profile with K6_PROFILE or pass a name to
getProfile().
| Profile | Stages (summary) | Typical use |
|---|---|---|
smoke |
2 VUs · ~35 s | Local gate, test:report suite |
ci |
3→5 VUs · ~55 s | GitHub Actions when CI=true and profile is smoke |
load |
10→25 VUs · 7 min | Sustained production-like traffic |
stress |
20→100 VUs · 12 min | Find degradation under high load |
spike |
5→100 VUs burst · ~2.5 min | Sudden traffic surge and recovery |
soak |
15 VUs · 30+ min steady | Endurance / memory leak detection |
breakpoint |
Stepped 10→125 VUs | Extended capacity ramp (K6_PROFILE=breakpoint) |
// config/profiles.js — excerpt
export const profiles = {
smoke: {
stages: [
{ duration: '10s', target: 2 },
{ duration: '20s', target: 2 },
{ duration: '5s', target: 0 },
],
gracefulRampDown: '5s',
},
load: {
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 25 },
{ duration: '2m', target: 25 },
{ duration: '1m', target: 0 },
],
gracefulRampDown: '30s',
},
// stress, spike, soak, breakpoint, ci …
};
export function getProfile(name) {
const key = name || __ENV.K6_PROFILE || 'smoke';
if (__ENV.CI === 'true' && key === 'smoke') return profiles.ci;
return profiles[key] || profiles.smoke;
}
Thresholds & checks
k6 uses two complementary mechanisms. A run fails if either breaches limits. See threshold-strategy.md for the full SLO matrix.
| Mechanism | Purpose | Example |
|---|---|---|
check() |
Functional correctness per request | health status 200, login has token |
thresholds |
Aggregate SLO over the whole run | http_req_duration: p(95)<1500, checks: rate>0.99 |
Per-endpoint tags
Tag every HTTP call with endpoint so filtered thresholds apply:
http.get(url, {
tags: { endpoint: 'health', name: 'GET /health' },
});
// config/thresholds.js
export const smokeThresholds = {
http_req_failed: ['rate<0.005'],
http_req_duration: ['p(95)<1500', 'avg<800'],
'http_req_duration{endpoint:health}': ['p(95)<500'],
'http_req_duration{endpoint:users}': ['p(95)<1500'],
'http_req_duration{endpoint:auth_login}': ['p(95)<2000'],
checks: ['rate>0.99'],
};
Checks in lib/http.js
| Helper | Checks applied |
|---|---|
getHealth() |
status 200; response < 1 s |
getUsers(token?) |
status 200; non-empty users[]; response < 2 s |
getStaticPage(path) |
status 200; Content-Type includes text/html |
login() in lib/auth.js |
status 200; JSON token is a string |
HTTP helpers
Centralized helpers in lib/http.js and lib/auth.js keep scenarios thin and
ensure consistent checks across every test type.
getHealth()
export function getHealth(params = {}) {
const res = http.get(`${BASE_URL}${endpoints.health}`, {
tags: { endpoint: 'health', name: 'GET /health' },
...params,
});
check(res, {
'health status 200': (r) => r.status === 200,
'health responds quickly': (r) => r.timings.duration < 1000,
});
return res;
}
getUsers(token?)
export function getUsers(token = null, params = {}) {
const headers = token ? authHeaders(token) : { 'Content-Type': 'application/json' };
const res = http.get(`${BASE_URL}${endpoints.users.list}`, {
headers,
tags: { endpoint: 'users', name: 'GET /api/users' },
...params,
});
check(res, {
'users status 200': (r) => r.status === 200,
'users has array': (r) => Array.isArray(r.json('users')) && r.json('users').length > 0,
'users responds within 2s': (r) => r.timings.duration < 2000,
});
return res;
}
login()
export function login(params = {}) {
const res = http.post(`${BASE_URL}${endpoints.auth.login}`, payload, {
headers: { 'Content-Type': 'application/json' },
tags: { endpoint: 'auth_login', name: 'POST /api/auth/login' },
...params,
});
check(res, {
'login status 200': (r) => r.status === 200,
'login has token': (r) => typeof r.json('token') === 'string',
});
return res.json('token');
}
getStaticPage(path)
export function getStaticPage(path, params = {}) {
const res = http.get(`${BASE_URL}${path}`, {
tags: { endpoint: 'static', name: `GET ${path}` },
...params,
});
check(res, {
'static page status 200': (r) => r.status === 200,
'static page has html': (r) => (r.headers['Content-Type'] || '').includes('text/html'),
});
return res;
}
Additional utilities: weightedPick(), thinkTime(), getErrorSimulation().
Test types
Each test type maps to a folder, load profile, and threshold set.
| Type | Script | npm script | What it validates |
|---|---|---|---|
| Smoke | |||
| Smoke | scenarios/smoke/api-health.js |
test:smoke |
Availability + baseline latency (health, users, login) |
| Load | |||
| Load — auth | scenarios/load/api-auth.js |
test:load:auth |
Sustained login throughput; login_duration, login_errors |
| Load — users | scenarios/load/api-users.js |
test:load:users |
Read-heavy users list with setup() token reuse |
| Load — mixed | scenarios/load/mixed-traffic.js |
test:load:mixed |
Weighted traffic: 35% health, 30% users, 15% static, 20% anon users |
| Stress · Spike · Soak | |||
| Stress | scenarios/stress/api-breakpoint.js |
test:stress |
Ramp to 100 VUs; counts degraded_responses |
| Spike | scenarios/spike/api-spike.js |
test:spike |
5→100 VU burst; spike_failures custom metric |
| Soak | scenarios/soak/api-endurance.js |
test:soak |
15 VUs steady for K6_SOAK_MINUTES (default 30 min) |
| Journey · Browser | |||
| Journey | journeys/login-flow.js |
test:journey:login |
Login page → API login → dashboard (grouped steps) |
| Journey | journeys/authenticated-user.js |
test:journey:auth |
Login → users → dashboard → team pages |
| Browser | scenarios/browser/login-page.js |
test:browser:login |
Chromium: fill data-testid login form; measure browser_page_load |
Custom metrics
Import Trend, Rate, and Counter from
k6/metrics to track scenario-specific behaviour beyond built-in HTTP metrics.
Trend
Timing distributions — p95, avg. Example: login_duration, journey_duration, browser_page_load.
Rate
Percentage of non-zero values. Example: login_errors, spike_failures.
Counter
Monotonically increasing count. Example: degraded_responses in stress scenario.
import { Trend, Rate, Counter } from 'k6/metrics';
const loginDuration = new Trend('login_duration', true);
const loginErrors = new Rate('login_errors');
const degradedResponses = new Counter('degraded_responses');
// Record values during the test
loginDuration.add(res.timings.duration);
loginErrors.add(!ok);
degradedResponses.add(1);
// Add thresholds in options
export const options = {
thresholds: {
...getThresholds('load'),
login_duration: ['p(95)<2000'],
login_errors: ['rate<0.01'],
},
};
| Metric | Type | Scenario | Threshold |
|---|---|---|---|
login_duration | Trend | api-auth.js | p(95)<2000 |
login_errors | Rate | api-auth.js | rate<0.01 |
users_list_duration | Trend | api-users.js | p(95)<2000 |
login_flow_duration | Trend | login-flow.js | p(95)<4000 |
spike_failures | Rate | api-spike.js | rate<0.10 |
degraded_responses | Counter | api-breakpoint.js | informational |
browser_page_load | Trend | login-page.js | p(95)<5000 |
group() for journeys
Use k6 group() to label steps in multi-step flows. Groups appear separately in the
end-of-test summary and HTML report, making it easy to pinpoint slow steps.
import { sleep, group } from 'k6';
import { Trend } from 'k6/metrics';
const loginFlowDuration = new Trend('login_flow_duration', true);
export default function loginFlow() {
const start = Date.now();
group('01_load_login_page', () => {
getStaticPage(endpoints.web.login);
sleep(0.5);
});
group('02_api_login', () => {
const res = http.post(`${BASE_URL}${endpoints.auth.login}`, payload, { /* … */ });
check(res, { 'login succeeds': (r) => r.status === 200 });
sleep(0.3);
});
group('03_post_login_navigation', () => {
getStaticPage(endpoints.web.dashboard);
sleep(0.5);
});
loginFlowDuration.add(Date.now() - start);
}
Journey scripts: journeys/login-flow.js (3 groups) and
journeys/authenticated-user.js (4 groups: login → users → dashboard → team).
setup() phase
The setup() function runs once before VUs start. Use it for expensive
one-time work — e.g. obtaining a shared auth token — and pass the result to the default function.
// lib/auth.js
export function setupAuth() {
const token = login();
return { token };
}
// scenarios/load/api-users.js
export function setup() {
return setupAuth();
}
export default function usersLoad(data) {
const res = getUsers(data.token);
usersListDuration.add(res.timings.duration);
sleep(0.5 + Math.random());
}
login() every iteration to simulate real auth load. Users list uses
setup() to focus on read throughput without login overhead.
Reports
testflow-k6 produces multiple report formats for local debugging and CI publishing.
npm run test:report
Runs 7 scenarios with K6_PROFILE=smoke (~4 min total):
- smoke · 2. load-auth · 3. load-users · 4. mixed-traffic
- login-flow · 6. authenticated-user · 7. spike
Generates:
results/REPORT.md— Markdown summaryresults/report/index.html— HTML dashboard with PASS/FAIL, charts, checks, thresholdsresults/summary-latest.json— machine-readable summaryresults/runs/<run-id>-*.json— raw k6 exports
npm run test:report
npm run report:open # open results/report/index.html locally
Live report: lflucasferreira.github.io/testflow-k6/report/
handleSummary
// lib/summary.js — exported from every scenario
export function handleSummary(data) {
return {
stdout: 'k6 summary text…',
'results/summary-latest.json': JSON.stringify(data, null, 2),
[`results/summary-${scenario}-${profile}-${timestamp}.json`]: JSON.stringify(data, null, 2),
};
}
k6 Web Dashboard (live + HTML export)
npm run test:smoke:ui # K6_DASHBOARD=true — live at http://localhost:5665
npm run test:load:mixed:ui
K6_DASHBOARD=true K6_SCENARIO=spike npm run test:spike
GitHub Pages
On every push to main, the publish-pages job in
.github/workflows/k6.yml builds the docs hub, runs the report suite, and deploys to
GitHub Pages.
Grafana + InfluxDB
Stream live k6 metrics to InfluxDB and visualize in Grafana — locally or in the cloud.
Local stack
npm run grafana:up # TestFlow + InfluxDB + Grafana (Docker)
npm run test:smoke:grafana # smoke with live metrics
npm run test:load:mixed:grafana # load with dashboard
npm run grafana:down
| Service | URL | Notes |
|---|---|---|
| Grafana | http://localhost:3000 |
Dashboard: K6 Test Results |
| InfluxDB | http://localhost:8086 |
org testflow, bucket k6 |
| TestFlow | http://localhost:5050 |
Same sandbox as other suites |
Cloud (InfluxDB Cloud + Grafana Cloud)
cp .env.influx.cloud.example .env.influx.cloud # add tokens
npm run test:smoke:influx:cloud
npm run test:load:mixed:influx:cloud
Full guide: grafana-cloud-setup.md · influxdb-cloud-dashboard.md · embed via docs/grafana/
CI — GitHub Actions
Workflow: .github/workflows/k6.yml
Smoke gate
Every push/PR runs api-health.js with CI=true and lighter ci profile.
Service container
qaschool/testflow:latest on port 5050 with health checks.
GitHub Pages
publish-pages job on push to main — docs hub + 7-scenario HTML report.
Manual load
workflow_dispatch — choose smoke, load, stress, or spike profile for mixed-traffic.
# k6.yml — smoke job (excerpt)
services:
testflow:
image: qaschool/testflow:latest
ports: ['5050:5050']
env:
CI: "true"
BASE_URL: http://localhost:5050
K6_PROFILE: smoke
DEMO_PASSWORD: ${{ secrets.DEMO_PASSWORD }}
run: k6 run --summary-export=results/summary.json scenarios/smoke/api-health.js
Badge: k6 CI workflow
npm scripts reference
| Script | Description |
|---|---|
| Smoke & CI | |
test:smoke | Smoke gate — api-health.js |
test:smoke:ui | Smoke with k6 web dashboard + HTML export |
test:ci | CI-style run with summary export |
| Load | |
test:load:auth | Sustained login throughput |
test:load:auth:ui | Load auth with web dashboard |
test:load:users | Read-heavy users list (setup token) |
test:load:mixed | Weighted mixed traffic |
test:load:mixed:ui | Mixed traffic with web dashboard |
| Stress · Spike · Soak | |
test:stress | Stress / breakpoint ramp |
test:spike | Spike burst traffic |
test:soak | Soak endurance (30+ min) |
| Journey · Browser | |
test:journey:auth | Authenticated user journey |
test:journey:login | Login flow journey |
test:browser:login | Browser login page (Chromium) |
| Reports | |
test:report | 7-scenario suite + HTML report |
report:open | Open latest HTML report in browser |
| Docker | |
docker:up | Start TestFlow via compose |
docker:down | Stop compose stack |
docker:smoke | Run smoke in k6 container |
docker:smoke:host | k6 container → host TestFlow on 5050 |
docker:load | Load mixed-traffic in container |
docker:stress | Stress in container |
| Grafana / InfluxDB | |
grafana:up | Start monitoring stack |
grafana:down | Stop monitoring stack |
grafana:build | Build k6-influx Docker image |
test:smoke:grafana | Smoke → local InfluxDB + Grafana |
test:smoke:influx:local | Smoke → Docker InfluxDB |
test:smoke:influx:cloud | Smoke → InfluxDB Cloud |
test:load:mixed:grafana | Load → local Grafana |
test:load:mixed:influx:local | Load → local InfluxDB |
test:load:mixed:influx:cloud | Load → InfluxDB Cloud |
| Docs | |
slides | Serve docs on port 3337 |
slides:open | Serve + open slides in browser |
docs:open | Serve docs hub on port 3338 |
slides:pdf | Export slides to PDF |
Training documentation
Block-by-block walkthroughs for every scenario script — ideal for students learning k6 and SLO-driven testing.
docs/en/README.md
English index — smoke, load, stress, spike, soak, journeys, browser walkthroughs.
docs/pt/README.md
Índice em português — mesma cobertura de cenários.
threshold-strategy.md
Checks vs thresholds, per-endpoint tags, profile alignment, custom metrics.
slides/
Reveal.js presentation — k6 fundamentals, test types, Grafana, CI/CD.
Scenario walkthroughs
| Scenario | EN doc | Script |
|---|---|---|
| API health | api-health.md | scenarios/smoke/api-health.js |
| Auth load | api-auth.md | scenarios/load/api-auth.js |
| Users load | api-users.md | scenarios/load/api-users.js |
| Mixed traffic | mixed-traffic.md | scenarios/load/mixed-traffic.js |
| Spike | api-spike.md | scenarios/spike/api-spike.js |
| Stress | api-breakpoint.md | scenarios/stress/api-breakpoint.js |
| Soak | api-endurance.md | scenarios/soak/api-endurance.js |
| Login journey | login-flow.md | journeys/login-flow.js |
| Auth journey | authenticated-user.md | journeys/authenticated-user.js |
| Browser login | login-page.md | scenarios/browser/login-page.js |
Best practices
- Reuse lib helpers — never duplicate HTTP calls or checks in scenario files
- Tag every request — set
endpointfor per-route SLO thresholds - Pick the right profile — smoke for gates, load for sustained traffic, spike for bursts
- Separate checks from thresholds — checks assert correctness; thresholds gate SLOs
- Export handleSummary — every scenario should export
handleSummaryfromlib/summary.js - Use group() in journeys — step-level timing makes reports actionable
- setup() for expensive auth — when the test focus is read/write, not login throughput
- Custom metrics with thresholds — add Trend/Rate thresholds only when built-in metrics are insufficient
- Register new scenarios — update
scripts/scenario-catalog.mjsand add EN/PT walkthroughs - Align with functional suites — SLO budgets mirror Cypress/Playwright timeouts
- CI stays fast — smoke profile only in PR gate; run load/stress manually or via workflow_dispatch
- Read threshold-strategy.md before loosening limits
- Browser tests need native k6 — Docker fallback does not support
--browser - Think time between requests — use
sleep()andthinkTime()for realistic pacing
Next steps
Interactive slides
Reveal.js presentation with animations, test type overview, Grafana setup, and CI/CD patterns.
Official documentation
grafana.com/docs/k6 — executors, metrics, browser module, extensions, and cloud.
Live report
GitHub Pages report — 7-scenario dashboard updated on every push to main.
Target application
qaschool/testflow — the sandbox app all TestFlow suites exercise.