K6 Cloud and HTML Reports

7 min read

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.js

k6 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.html

The 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-reporter from raw GitHub URLs in production. The URL https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js always 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 cloud against internal staging environments. Grafana Cloud K6 sends traffic from external cloud servers — your staging environment must be publicly accessible. Use k6 run --out cloud instead 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.

  1. Write a script with vus: 5, duration: '30s' making requests to /posts, /users, and /albums. Tag each request. Add thresholds: p(95)<300 for each endpoint.
  2. Add handleSummary that outputs:
    • report.html using htmlReport
    • stdout using textSummary
  3. Run the test. Open report.html in a browser. Find: the threshold pass/fail status, the p(95) for each tagged endpoint, and the total request count.
  4. Add a JSON executive summary file: extract p(95), error rate, and total requests into a simple object and write it to executive-summary.json.
  5. 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.
  6. If you have a Grafana Cloud K6 account (free tier), run k6 cloud script.js (after k6 cloud login --token YOUR_TOKEN) and observe the built-in cloud dashboard. Compare it with the HTML report you generated locally.

// tip to track lessons you complete and pick up where you left off across devices.