Master k6 thresholds with abortOnFail and custom metrics, connect k6 to InfluxDB and Grafana for real-time dashboards, and tag requests for endpoint-level SLO monitoring.
Running k6 without a dashboard is like driving without a speedometer. You can see crashes in the terminal summary, but you cannot see trends developing, latency spikes at specific endpoints, or the exact moment the database connection pool exhausted. k6 integrates natively with InfluxDB and Grafana to stream real-time metrics during test execution — giving you the observability needed to diagnose issues during the test, not after it completes.
This lesson covers the full monitoring stack: k6 custom metrics, request tagging for endpoint-level granularity, InfluxDB output configuration, the official Grafana dashboard for k6, and writing handleSummary for CI-parseable results.
Basic thresholds check the final test result. Advanced thresholds abort the test mid-run when the system is already failing — saving time and infrastructure costs.
1export const options = {
2 scenarios: {
3 load: { executor: 'ramping-vus', stages: [
4 { duration: '5m', target: 100 },
5 { duration: '20m', target: 100 },
6 ]},
7 },
8 thresholds: {
9 // Standard threshold — evaluated at test end
10 http_req_duration: ['p(95)<500', 'p(99)<1500'],
11
12 // AbortOnFail — stop test if error rate exceeds 5%
13 // delayAbortEval: wait 30s before confirming (handles transient spikes)
14 http_req_failed: [
15 {
16 threshold: 'rate<0.05',
17 abortOnFail: true,
18 delayAbortEval: '30s',
19 },
20 ],
21
22 // Multiple conditions — ALL must pass
23 checkout_duration_ms: [
24 'p(95)<1000',
25 { threshold: 'p(99)<3000', abortOnFail: true, delayAbortEval: '60s' },
26 ],
27 },
28};When abortOnFail triggers, k6 still runs teardown() and handleSummary() — so cleanup and results export happen even on early termination. The exit code is still 99, failing the CI step.
k6 has four custom metric types that feed into dashboards and thresholds:
Rate: Tracks the fraction of values that are truthy. Use for error rates, cache hit rates, check pass rates.
Trend: Tracks statistics (min, max, avg, percentiles) over a series of values. Use for custom durations and latencies.
Counter: Monotonically increasing count. Use for business events (orders, signups, payments).
Gauge: Current value at any point in time. Use for queue depth, active sessions, connection count.
1import { Rate, Trend, Counter, Gauge } from 'k6/metrics';
2
3// Declare custom metrics at module level
4const paymentErrors = new Rate('payment_errors');
5const checkoutLatency = new Trend('checkout_latency_ms', true); // 'true' = isTime (enables percentiles)
6const successfulOrders = new Counter('orders_completed');
7const activeUsers = new Gauge('active_users_gauge');
8
9export const options = {
10 thresholds: {
11 payment_errors: ['rate<0.001'], // <0.1% payment failures
12 checkout_latency_ms: ['p(95)<800'], // checkout p95 under 800ms
13 orders_completed: ['count>100'], // must complete 100+ orders
14 },
15};
16
17export default function () {
18 activeUsers.add(1);
19
20 const start = Date.now();
21 const res = http.post('https://api.example.com/checkout', payload);
22 const duration = Date.now() - start;
23
24 checkoutLatency.add(duration);
25 paymentErrors.add(res.status >= 400);
26
27 if (res.status === 201) {
28 successfulOrders.add(1);
29 }
30
31 activeUsers.add(-1);
32}The k6 → InfluxDB → Grafana pipeline streams metrics in real-time, enabling you to watch dashboards during test execution.
1# docker-compose.yml — local monitoring stack
2version: '3.8'
3services:
4 influxdb:
5 image: influxdb:2.7
6 ports: ['8086:8086']
7 environment:
8 DOCKER_INFLUXDB_INIT_MODE: setup
9 DOCKER_INFLUXDB_INIT_USERNAME: admin
10 DOCKER_INFLUXDB_INIT_PASSWORD: password123
11 DOCKER_INFLUXDB_INIT_ORG: myorg
12 DOCKER_INFLUXDB_INIT_BUCKET: k6
13 DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: mytoken
14
15 grafana:
16 image: grafana/grafana:latest
17 ports: ['3001:3000']
18 environment:
19 GF_AUTH_ANONYMOUS_ENABLED: 'true'
20 GF_AUTH_ANONYMOUS_ORG_ROLE: Admin1# Start monitoring stack
2docker-compose up -d influxdb grafana
3
4# Run k6 with InfluxDB v2 output
5K6_INFLUXDB_ORGANIZATION=myorg \
6K6_INFLUXDB_BUCKET=k6 \
7K6_INFLUXDB_TOKEN=mytoken \
8k6 run --out influxdb=http://localhost:8086 tests/perf/load.js1// In your k6 script: add meaningful tags for Grafana filtering
2http.get('https://api.example.com/products', {
3 tags: {
4 name: 'list-products', // shows in Grafana endpoint dropdown
5 endpoint: 'GET-products', // secondary filter
6 team: 'catalog', // ownership filter
7 },
8});In Grafana, import dashboard ID 2587 (the official k6 Grafana dashboard) and connect it to your InfluxDB datasource. You will see real-time VU count, RPS, error rate, p50/p95/p99 latency, and custom metrics.
Tags turn a global dashboard into endpoint-level visibility. Without tags, you see 'the API is slow' — with tags you see 'GET /search is slow but POST /orders is fine'.
1import http from 'k6/http';
2
3// Batch multiple endpoints with shared params — cleaner and faster
4const responses = http.batch([
5 [
6 'GET',
7 'https://api.example.com/products',
8 null,
9 { tags: { name: 'list-products', http_method: 'GET' } },
10 ],
11 [
12 'GET',
13 'https://api.example.com/categories',
14 null,
15 { tags: { name: 'list-categories', http_method: 'GET' } },
16 ],
17 [
18 'GET',
19 'https://api.example.com/featured',
20 null,
21 { tags: { name: 'featured-products', http_method: 'GET' } },
22 ],
23]);
24
25// Tag-based thresholds enforce per-endpoint SLOs
26export const options = {
27 thresholds: {
28 'http_req_duration{name:list-products}': ['p(95)<300'],
29 'http_req_duration{name:list-categories}': ['p(95)<200'],
30 'http_req_duration{name:featured-products}':['p(95)<400'],
31 // Without tags, a slow /featured would hide behind fast /categories avg
32 },
33};Important: The name tag is special in k6 — it groups URLs in the summary. Use it consistently. Other tags (team, version, env) are for Grafana filtering only.
handleSummary() intercepts the end-of-test summary and lets you transform it into any format: JSON for CI parsing, HTML for reports, CSV for spreadsheets.
1import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
2
3export function handleSummary(data) {
4 // Determine overall pass/fail from thresholds
5 const allPassed = Object.values(data.metrics).every((metric) => {
6 if (!metric.thresholds) return true;
7 return Object.values(metric.thresholds).every((t) => t.ok);
8 });
9
10 // Build a structured summary for CI
11 const summary = {
12 passed: allPassed,
13 timestamp: new Date().toISOString(),
14 duration: data.state.testRunDuration,
15 scenarios: Object.keys(data.state || {}),
16 slos: {
17 p95Latency: data.metrics.http_req_duration?.values['p(95)'],
18 errorRate: data.metrics.http_req_failed?.values.rate,
19 totalRequests: data.metrics.http_reqs?.values.count,
20 },
21 thresholds: Object.fromEntries(
22 Object.entries(data.metrics)
23 .filter(([, m]) => m.thresholds)
24 .map(([name, m]) => [name, Object.fromEntries(
25 Object.entries(m.thresholds).map(([k, v]) => [k, v.ok])
26 )])
27 ),
28 };
29
30 return {
31 // Machine-readable for CI scripts
32 'tests/perf/results/summary.json': JSON.stringify(summary, null, 2),
33 // Human-readable terminal output
34 stdout: textSummary(data, { indent: ' ', enableColors: true }),
35 // Optional: HTML report
36 // 'tests/perf/results/report.html': htmlReport(data),
37 };
38}In CI, parse summary.json to set build status:
1if [ $(jq .passed tests/perf/results/summary.json) == 'false' ]; then
2 echo 'Performance SLOs breached — blocking deploy'
3 exit 1
4fiComplete setup: Docker Compose for the monitoring stack, k6 script with custom metrics and tags, and handleSummary for CI-parseable results.
1// tests/perf/monitored-load.js
2// Full monitoring setup with InfluxDB streaming and CI-ready summary
3// Run: K6_INFLUXDB_TOKEN=mytoken k6 run --out influxdb=http://localhost:8086 tests/perf/monitored-load.js
4
5import http from 'k6/http';
6import { check, sleep, group } from 'k6';
7import { Rate, Trend, Counter } from 'k6/metrics';
8import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
9
10// Custom business metrics — visible in Grafana
11const authFailures = new Rate('auth_failures');
12const courseLoadTime = new Trend('course_load_ms', true);
13const enrollmentSuccesses = new Counter('enrollments_total');
14
15export const options = {
16 scenarios: {
17 main: {
18 executor: 'ramping-vus',
19 startVUs: 1,
20 stages: [
21 { duration: '3m', target: 50 },
22 { duration: '10m', target: 50 },
23 { duration: '3m', target: 0 },
24 ],
25 },
26 },
27 thresholds: {
28 // Infrastructure SLOs
29 http_req_duration: ['p(95)<800', 'p(99)<2000'],
30 http_req_failed: ['rate<0.005'],
31 // Business SLOs
32 auth_failures: ['rate<0.001'],
33 course_load_ms: ['p(95)<500'],
34 enrollments_total: ['count>50'],
35 // Per-endpoint SLOs via tags
36 'http_req_duration{name:login}': ['p(95)<300'],
37 'http_req_duration{name:course-list}':['p(95)<400'],
38 'http_req_duration{name:enroll}': ['p(95)<600'],
39 },
40};
41
42const BASE = __ENV.BASE_URL || 'http://localhost:3000';
43
44export function setup() {
45 // Verify service is up before running load
46 const res = http.get(`${BASE}/api/health`);
47 if (res.status !== 200) throw new Error('Service not healthy — aborting test');
48 console.log('Service healthy, starting load test');
49}
50
51export default function () {
52 group('Authentication', () => {
53 const loginRes = http.post(
54 `${BASE}/api/auth/login`,
55 JSON.stringify({ email: '[email protected]', password: 'TestPass123!' }),
56 { headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } }
57 );
58 authFailures.add(loginRes.status !== 200);
59 if (loginRes.status !== 200) return;
60
61 const token = loginRes.json('accessToken');
62 const h = {
63 Authorization: `Bearer ${token}`,
64 'Content-Type': 'application/json',
65 };
66
67 sleep(0.5);
68
69 group('Course browsing', () => {
70 const start = Date.now();
71 const coursesRes = http.get(
72 `${BASE}/api/courses?limit=12&page=1`,
73 { headers: h, tags: { name: 'course-list' } }
74 );
75 courseLoadTime.add(Date.now() - start);
76 check(coursesRes, {
77 'courses 200': (r) => r.status === 200,
78 'has courses': (r) => r.json('data.length') > 0,
79 });
80
81 // Enroll in first available course
82 if (coursesRes.status === 200) {
83 const firstCourseId = coursesRes.json('data.0.id');
84 const enrollRes = http.post(
85 `${BASE}/api/progress/enroll`,
86 JSON.stringify({ courseId: firstCourseId }),
87 { headers: h, tags: { name: 'enroll' } }
88 );
89 if (enrollRes.status === 201) {
90 enrollmentSuccesses.add(1);
91 }
92 }
93 });
94 });
95
96 sleep(1 + Math.random() * 2); // 1-3s think time
97}
98
99export function handleSummary(data) {
100 const passed = Object.values(data.metrics).every((m) =>
101 !m.thresholds || Object.values(m.thresholds).every((t) => t.ok)
102 );
103
104 const report = {
105 passed,
106 timestamp: new Date().toISOString(),
107 key_metrics: {
108 p95_latency_ms: data.metrics.http_req_duration?.values['p(95)'],
109 error_rate_pct: (data.metrics.http_req_failed?.values.rate * 100).toFixed(3),
110 total_requests: data.metrics.http_reqs?.values.count,
111 auth_failure_rate: data.metrics.auth_failures?.values.rate,
112 enrollments: data.metrics.enrollments_total?.values.count,
113 },
114 };
115
116 return {
117 'tests/perf/results/monitored-summary.json': JSON.stringify(report, null, 2),
118 stdout: textSummary(data, { indent: ' ', enableColors: true }),
119 };
120}This script demonstrates the full production monitoring pattern: custom business metrics (auth_failures, course_load_ms, enrollments_total) that have business meaning beyond raw HTTP metrics; request tagging with name labels for endpoint-level Grafana visibility; a setup() function that verifies service health before starting load; and handleSummary generating a structured JSON report with key_metrics extracted for easy CI parsing. Start the monitoring stack with docker-compose up influxdb grafana, then run k6 with --out influxdb=http://localhost:8086 to stream metrics in real time to Grafana dashboard 2587.
The next lesson covers data-driven k6 tests — SharedArray for large datasets, CSV parameterization, and randomized test data patterns that model realistic user behavior.
Your progress is saved in this browser, on this device only.