Local K6 runs test from a single machine on your network. Grafana Cloud K6 distributes the load from multiple geographic regions and provides built-in dashboards without managing any infrastructure. HTML reports bridge the gap when you need to share results with people who do not have Grafana access.
Three reporting approaches
K6 reporting options
Grafana (self-hosted)
Real-time dashboards during test execution
Persistent history across runs
Requires: Docker or server, InfluxDB, Grafana setup
Best for: engineering teams with existing Grafana infra
Cost: infrastructure only
Grafana Cloud K6
Managed — no infrastructure to maintain
Distributed load from multiple geographic regions
Built-in dashboard, team sharing, test comparison
Best for: production testing, cross-region simulation
Cost: free tier 50 VUh/month, paid beyond that
HTML reports
Self-contained file — no server required
Open in any browser, email as attachment
Generated at test end via handleSummary
Best for: stakeholder reports, CI artifacts, ticket attachments
Cost: free (community library)
Grafana Cloud K6
Grafana Cloud K6 (formerly k6 Cloud) runs your test script in Grafana's cloud infrastructure. Load is distributed across their servers — from one or more geographic regions — giving you results that reflect real-world network latency rather than your CI runner's internal network.
# Authenticate (one-time)
k6 cloud login --token YOUR_API_TOKEN
# Run in the cloud
k6 cloud script.js
# Or run locally but stream results to Grafana Cloud
k6 run --out cloud script.jsk6 cloud runs the test in the cloud. k6 run --out cloud runs locally but streams results to your Grafana Cloud dashboard for real-time visualisation and persistent storage.
When to use Grafana Cloud K6:
- Testing from outside your network (production smoke tests after deploys)
- Simulating users in specific geographic regions (EU, US, APAC)
- Testing with distributed load from multiple geographic origin points
- Teams without existing Grafana/InfluxDB infrastructure
- Load tests that exceed what a single CI runner can generate
Free tier limits: 50 VU-hours per month. A 30-minute test with 100 VUs = 50 VUh — the entire free monthly allowance in one run.
HTML reports with handleSummary
For distributable reports without cloud infrastructure:
import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
export function handleSummary(data) {
return {
'report.html': htmlReport(data, { title: 'Checkout API — Load Test Report' }),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}The HTML file is self-contained — all CSS and charts are embedded. Open it in any browser, email it, or attach it to a Jira ticket. It shows:
- Pass/fail status for each threshold
- Summary statistics for every metric (avg, min, max, p(90), p(95), p(99))
- Check pass rates
- HTTP status code breakdown
Tailoring reports to the audience
The same test run produces different report types for different audiences:
export function handleSummary(data) {
// Engineering: full JSON for programmatic analysis
// QA: HTML report with metric breakdowns
// Both: normal terminal output preserved
return {
'engineering-report.json': JSON.stringify(data, null, 2),
'qa-report.html': htmlReport(data, { title: 'Sprint 42 — Load Test' }),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}For executive or stakeholder summaries, extract only the key metrics into a simpler structure:
export function handleSummary(data) {
const summary = {
testDate: new Date().toISOString(),
durationSeconds: data.state.testRunDurationMs / 1000,
p95LatencyMs: data.metrics['http_req_duration']?.values['p(95)'],
errorRate: data.metrics['http_req_failed']?.values['rate'],
totalRequests: data.metrics['http_reqs']?.values['count'],
checkPassRate: data.metrics['checks']?.values['rate'],
allThresholdsMet: !Object.values(data.metrics).some(m => m.thresholds && !m.thresholds.ok),
};
return {
'executive-summary.json': JSON.stringify(summary, null, 2),
'full-report.html': htmlReport(data),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}CI/CD integration
In a CI pipeline, the HTML report becomes a build artifact and the exit code determines pipeline pass/fail:
# GitHub Actions example
- name: Run load test
run: k6 run --vus 50 --duration 5m tests/load-test.js
- name: Upload load test report
if: always() # upload even on test failure
uses: actions/upload-artifact@v3
with:
name: k6-load-test-report
path: report.htmlThe if: always() ensures the HTML report is captured even when the K6 run exits with code 108 (threshold failure) — which would otherwise abort the job before the upload step.
Sharing reports
For engineers: Link to the Grafana dashboard (requires access) or attach engineering-report.json to the PR.
For QA teams: Email or Slack the HTML report. It opens in any browser without installation.
For releases: Attach the HTML report to the release ticket or Confluence page. The file is self-contained and renders the same regardless of where it is opened.
For trend tracking: Commit the JSON summary to a repository with the test date in the filename (report-2024-01-15.json). Build a trend visualisation over time without needing a running InfluxDB.
⚠️ Common mistakes
- Importing
k6-reporterfrom raw GitHub URLs in production. The URLhttps://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.jsalways pulls the latest version, which can change. Pin to a specific commit SHA or tag for reproducible reports. - Not using
if: always()in CI for report upload. When a threshold fails, K6 exits with code 108, which many CI systems treat as a job failure and skip subsequent steps. The HTML report is most useful when the test fails — ensure the upload step runs unconditionally. - Running
k6 cloudagainst internal staging environments. Grafana Cloud K6 sends traffic from external cloud servers — your staging environment must be publicly accessible. Usek6 run --out cloudinstead if you need to test internal systems while still streaming results to the cloud dashboard.
🎯 Practice task
Build a complete reporting setup with multiple output targets. 30 minutes.
Use https://jsonplaceholder.typicode.com.
- Write a script with
vus: 5, duration: '30s'making requests to/posts,/users, and/albums. Tag each request. Add thresholds:p(95)<300for each endpoint. - Add
handleSummarythat outputs:report.htmlusinghtmlReportstdoutusingtextSummary
- Run the test. Open
report.htmlin a browser. Find: the threshold pass/fail status, the p(95) for each tagged endpoint, and the total request count. - Add a JSON executive summary file: extract
p(95), error rate, and total requests into a simple object and write it toexecutive-summary.json. - Intentionally fail a threshold by tightening it to
p(95)<1. Run again. Confirm K6 exits with code 108 (echo $?in the terminal). Confirm the HTML report is still generated despite the failure. - If you have a Grafana Cloud K6 account (free tier), run
k6 cloud script.js(afterk6 cloud login --token YOUR_TOKEN) and observe the built-in cloud dashboard. Compare it with the HTML report you generated locally.