# Share a k6 Load Test Report — Stable Link, Run-to-Run Diffs

Canonical: https://commareports.com/share-k6-load-test-report
Published: 2026-08-19

> k6's terminal summary scrolls away and the HTML export lands in a CI artifact zip. Publish each load test to a stable URL with a revision per run, so p95 regressions are a diff instead of an argument.

# 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:

```javascript
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](/docs/api-tokens) (`reports:write` only) in your CI
secret store, create the report once, and `PATCH` it every run:

```bash
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](/docs/api) 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](/ci/github-actions-html-report),
[GitLab CI](/ci/gitlab-ci-html-report),
[Jenkins](/ci/jenkins-html-report),
[CircleCI](/ci/circleci-html-report).

## 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](/comment-on-html).

Add a [webhook](/docs/api) 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](/features/routines) 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](/features/routines/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](/docs/sharing).

## Try it

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

**[Create your first report →](https://commareports.com/)**

### Related

- [Share a Lighthouse report](/share-lighthouse-report) — the other performance number people argue about
- [Share a Playwright report](/share-playwright-report)
- [Publish from CI](/docs/ci)
