Share a k6 load test report

A k6 run ends with one of the best terminal summaries in the tooling world — checks, thresholds, http_req_duration percentiles, all of it. Then the pipeline finishes, the log scrolls into the archive, and the only record of whether last week's deploy moved p95 is somebody's memory.

Load tests are comparative by nature. A p95 of 340 ms means nothing on its own; it means everything against last Thursday's 180 ms. But the artifacts a k6 run produces by default are the worst possible substrate for comparison: a CI log that expires, or an HTML file inside an artifact zip.

Step 1 — get HTML out of k6

handleSummary is the hook. k6 calls it once at the end of the run with the full aggregated summary and writes whatever files you return:

export function handleSummary(data) {
  const p95 = data.metrics.http_req_duration.values["p(95)"].toFixed(1);
  const failRate = (data.metrics.http_req_failed.values.rate * 100).toFixed(2);

  return {
    "summary.json": JSON.stringify(data, null, 2),
    "summary.html": `
      <h1>Checkout load test</h1>
      <table>
        <tr><th>Requests</th><td>${data.metrics.http_reqs.values.count}</td></tr>
        <tr><th>p95 duration</th><td>${p95} ms</td></tr>
        <tr><th>Failure rate</th><td>${failRate}%</td></tr>
      </table>`,
  };
}

Keep it plain HTML — tables, headings, numbers. That's what a reviewer wants to point at, and it's what survives sanitization on publish.

Step 2 — publish it to a URL that doesn't move

Store a scoped token (reports:write only) in your CI secret store, create the report once, and PATCH it every run:

k6 run load/checkout.js

curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html summary.html \
        --arg title "Checkout load test — $GIT_SHA" '{title: $title, html: $html}')"

Attach summary.json as an asset if you want the raw metrics to travel with the document — 25 MB per file.

Publish on failure too (if: always() in GitHub Actions, when: always in GitLab and CircleCI, post { always { … } } in Jenkins). A run that blew its thresholds is the run people need to see. Provider wiring: GitHub Actions, GitLab CI, Jenkins, CircleCI.

Step 3 — let the history do the arguing

One report id per scenario means every run appends a revision at the same URL. Two consequences:

  • Regressions are a diff. Compare this run against the last green one and the p95 line that moved is highlighted. Nobody has to open two CI logs side by side and squint.
  • The thread lives on the number. A reviewer highlights the failure-rate row and pins a comment — "this spike is the new fraud check, expected, we're tuning the timeout in #1180." Three weeks later, when someone asks about the same spike, the answer is attached to it rather than lost in Slack. See commenting on HTML.

Add a webhook on revision.created and each run announces itself in Slack — the notification points at the report instead of replacing it.

Running it on a schedule

Load tests that only run before a release tell you about releases, not about drift. A routine can run the publish on a schedule — nightly, weekly — so the trend line exists whether or not anyone remembered to kick off a run. See scheduled HTML reports.

Keep it internal

Throughput ceilings, error rates and endpoint names are a fairly precise description of your system's capacity and its soft spots. Default the visibility to team or domain-gated rather than public — see the sharing model.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Create your first report →

Related