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 · Browser

What 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_PASSWORD env var
Unlike browser-only tools, k6 excels at HTTP throughput and concurrency. Use 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.

$ node --version
v20.x.x

$ curl -sf http://localhost:5050/health
{"status":"ok"}

$ k6 version
k6 v1.x.x (...)

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

$ k6 version
k6 v1.0.0 (go1.22.0, darwin/arm64)

$ npm run test:smoke
# uses local k6 when available
Corporate proxy (Zscaler / SSL): if 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.

  1. Start TestFlow
    docker run --rm -p 5050:5050 qaschool/testflow:latest
  2. Run the smoke scenario
    npm run test:smoke
  3. 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 probe
  • GET /api/users — read-heavy list with JSON validation
  • POST /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;
}
K6_PROFILE=load npm run test:load:mixed K6_PROFILE=spike npm run test:spike CI=true npm run test: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_durationTrendapi-auth.jsp(95)<2000
login_errorsRateapi-auth.jsrate<0.01
users_list_durationTrendapi-users.jsp(95)<2000
login_flow_durationTrendlogin-flow.jsp(95)<4000
spike_failuresRateapi-spike.jsrate<0.10
degraded_responsesCounterapi-breakpoint.jsinformational
browser_page_loadTrendlogin-page.jsp(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());
}
setup() vs per-iteration login: smoke and mixed-traffic scenarios call 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):

  1. smoke · 2. load-auth · 3. load-users · 4. mixed-traffic
  2. login-flow · 6. authenticated-user · 7. spike

Generates:

  • results/REPORT.md — Markdown summary
  • results/report/index.html — HTML dashboard with PASS/FAIL, charts, checks, thresholds
  • results/summary-latest.json — machine-readable summary
  • results/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/

GitHub Pages cannot host InfluxDB or Grafana. Only a public Grafana Cloud URL can be embedded on the static site.

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:smokeSmoke gate — api-health.js
test:smoke:uiSmoke with k6 web dashboard + HTML export
test:ciCI-style run with summary export
Load
test:load:authSustained login throughput
test:load:auth:uiLoad auth with web dashboard
test:load:usersRead-heavy users list (setup token)
test:load:mixedWeighted mixed traffic
test:load:mixed:uiMixed traffic with web dashboard
Stress · Spike · Soak
test:stressStress / breakpoint ramp
test:spikeSpike burst traffic
test:soakSoak endurance (30+ min)
Journey · Browser
test:journey:authAuthenticated user journey
test:journey:loginLogin flow journey
test:browser:loginBrowser login page (Chromium)
Reports
test:report7-scenario suite + HTML report
report:openOpen latest HTML report in browser
Docker
docker:upStart TestFlow via compose
docker:downStop compose stack
docker:smokeRun smoke in k6 container
docker:smoke:hostk6 container → host TestFlow on 5050
docker:loadLoad mixed-traffic in container
docker:stressStress in container
Grafana / InfluxDB
grafana:upStart monitoring stack
grafana:downStop monitoring stack
grafana:buildBuild k6-influx Docker image
test:smoke:grafanaSmoke → local InfluxDB + Grafana
test:smoke:influx:localSmoke → Docker InfluxDB
test:smoke:influx:cloudSmoke → InfluxDB Cloud
test:load:mixed:grafanaLoad → local Grafana
test:load:mixed:influx:localLoad → local InfluxDB
test:load:mixed:influx:cloudLoad → InfluxDB Cloud
Docs
slidesServe docs on port 3337
slides:openServe + open slides in browser
docs:openServe docs hub on port 3338
slides:pdfExport 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 healthapi-health.mdscenarios/smoke/api-health.js
Auth loadapi-auth.mdscenarios/load/api-auth.js
Users loadapi-users.mdscenarios/load/api-users.js
Mixed trafficmixed-traffic.mdscenarios/load/mixed-traffic.js
Spikeapi-spike.mdscenarios/spike/api-spike.js
Stressapi-breakpoint.mdscenarios/stress/api-breakpoint.js
Soakapi-endurance.mdscenarios/soak/api-endurance.js
Login journeylogin-flow.mdjourneys/login-flow.js
Auth journeyauthenticated-user.mdjourneys/authenticated-user.js
Browser loginlogin-page.mdscenarios/browser/login-page.js

Best practices

  • Reuse lib helpers — never duplicate HTTP calls or checks in scenario files
  • Tag every request — set endpoint for 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 handleSummary from lib/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.mjs and 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() and thinkTime() 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.