skip to content

In k6, what are the tradeoffs of uploading the report from inside `handleSummary()` rather than returning a file?

level: principalimportance: nice to knowfreq 28%

answer

  1. the hook is a real VU
  2. its samples are thrown away
  3. the upload can fail quietly
  4. time charged to every run
  5. returning bytes keeps delivery outside

basics

~20 s

The hook runs in a real VU with the full k6 API, so an upload works, but its samples are discarded, its time counts against handleSummaryTimeout, and a failure is only logged — so the upload can fail silently while the run stays green.

solid answer

~40 s

k6 executes `handleSummary()` inside a VU it creates for the purpose, so the whole module surface is available and the function may be `async`: sending the report over HTTP from inside the hook genuinely works. What you take on is a set of one-way costs. Samples emitted during the hook are discarded, so the request is invisible to every output. The call sits under `handleSummaryTimeout`, so a slow endpoint stretches the end of every run. And a failure — a throw, a bad status, a timeout — is only logged, so the exit code stays green. Returning bytes keeps the hook's job small and pushes delivery, retries and failure reporting to whatever invoked k6.

code

javascript · 15 lines
javascript
import http from 'k6/http';

export const options = { handleSummaryTimeout: '20s' };

export default function () {}

export function handleSummary(data) {
  const body = JSON.stringify(data);
  const res = http.post('https://reports.example.com/runs', body);
  if (res.status !== 200) {
    console.error('summary upload failed: ' + res.status);
  }
  // Keep a local copy so a failed upload still leaves evidence.
  return { 'summary.json': body };
}

go deeper

for a junior

Know that the hook can do more than format text, and that returning a file key is the simplest way to get a report out of a k6 run.

for a middle

Explain the mechanics that make an in-hook upload risky: discarded samples, a bounded call, and errors that are logged instead of failing the run.

for a senior

Show how you would keep such an upload honest — check the response, keep a local copy in the same returned map, and set a timeout that fails fast.

for a principal

Take a position for the whole estate: what the standard script returns, who may change the summary mode, and where the check that the report actually arrived lives.

## What the summary VU can actually do `handleSummary()` is not a restricted callback. k6 creates a fresh VU for it at the end of the run, re-executes the script's init context for that VU, and runs the function on a real event loop. Concretely: - the full k6 module surface is available, so the hook can make HTTP requests; - the hook may be declared `async`, and k6 unwraps the promise it returns before reading the map; - timers scheduled inside the hook fire, because k6 drains the event loop before taking the result; - the whole call is bounded by `handleSummaryTimeout`, 120s by default. So "post the report to a service" is a supported shape, not a hack. The question is whether it is the shape you want as a default. ## The costs you take on - **Its samples are discarded.** The summary VU's metrics go to a channel k6 throws away — the figures were aggregated before the hook ran. The upload request appears in no output and no metric. - **Its time is the run's time.** The call is charged to the end of every run, and a slow endpoint makes every pipeline invocation longer. - **Its failures are quiet.** A throw is logged and k6 falls back to printing the default text summary; a bad response status is not an error at all unless you check it; a timeout logs and produces nothing. **None of these change the process's exit code.** - **It can be switched off from outside.** `--summary-mode=disabled` skips the hook entirely, so a run-side flag can silently stop your reporting without touching the script. - **It needs credentials and network reach** in every environment the script runs in, which a file does not. - **It runs in a fresh VU.** k6 creates a VU specifically for the summary and re-executes the script's init context for it, so module-level setup is paid one more time at the end of the run. ## What the hook can see The object k6 passes in is the hook's whole world as far as run data goes. Because the summary VU is created from scratch, values a module accumulated while the test was running are back at their initial state — a counter the default function incremented is not there to be read. Anything the report needs must come from the argument or be computed inside the hook itself, which is one more reason to keep the hook's job to rendering and returning. ## Returning bytes versus sending them | concern | return a file key | send from inside the hook | |---|---|---| | what the hook does | renders content and returns it | renders, transmits, and interprets a response | | failure visibility | absent file, easy to assert on | logged only, unless you assert yourself | | effect on run duration | negligible | bounded only by `handleSummaryTimeout` | | retries | belong to whatever consumes the file | must be written in the hook | | environment coupling | a writable path | endpoint, credentials, network egress | | observability of the transfer | outside k6 entirely | invisible, since the hook's samples are discarded | ## Where the judgment lands 1. **Default to returning content.** The hook's declared contract is a map of destination to bytes; keeping it to that makes the script portable and makes "did it work?" a file-existence question. 2. **Send from the hook when there is no shared filesystem** between the run and whoever needs the report, and when adding a step around k6 is not available to you. That is a real situation, not a failure of discipline. 3. **If you do send, make the failure loud yourself.** Inspect the response, and write the payload to a path key as well so a failed transfer still leaves evidence. The map can hold both entries. 4. **Set the timeout deliberately.** A network call in the hook is exactly the case the 120-second default was not chosen for; pick a value that fails fast. 5. **Decide it once for the organisation.** The hook lives in the script, so whatever a shared template does becomes every team's reporting behaviour — including its silent failure mode. ## Version notes In k6 v2 this is all script-side: `handleSummaryTimeout` in the exported `options`, the map contract unchanged, and `--summary-mode=disabled` as the run-side switch that removes the call. The older `--summary-export=<file>` flag still exists and writes a fixed JSON shape, but it offers no choice of format and no second destination, which is why the hook is the general answer.

  • Do requests made inside `handleSummary()` show up in the k6 metrics?
    No. k6 gives the summary VU a sample channel it discards, because the aggregated figures were computed before the hook was called. The request is invisible to every output and to the summary itself.
  • If the upload returns a 500, does the k6 run fail?
    Not on its own. A response status is just data in the hook; k6 only fails a run through mechanisms decided before the summary is built. If you want the failure to matter you have to raise it yourself, and even a throw is logged rather than fatal.
  • What still writes a report if someone runs the script with `--summary-mode=disabled`?
    Nothing from k6. Disabled mode suppresses summary generation, which includes calling the hook, so neither the upload nor the file happens. That is an argument for owning how k6 is invoked, not just what the script exports.

saying these in an interview costs you the question

  • Thinks the hook cannot make network calls at all
  • Expects the hook's own requests to appear in the metrics
  • Assumes a failed upload turns the run red
  • Ignores that the hook's time is bounded by a timeout
  • Treats an in-hook upload as the only option without a shared disk