skip to content

Closing Report

What k6 prints and writes once the last iteration finishes, and how much of that a script controls. Asked because the default console block is not an artefact anyone can keep.

on this pageshow

explore

questions

9

In k6, what must an exported `handleSummary(data)` function return, and where does each entry go?

level: juniorimportance: must knowfreq 72%

answer

  1. return a map, not a report
  2. keys name destinations
  3. stdout and stderr are reserved
  4. values: string or ArrayBuffer
  5. any other key is a file path

basics

~20 s

An object whose keys are destinations and whose values are strings or ArrayBuffers. k6 sends the key stdout to standard output, stderr to standard error, and treats every other key as a file path it creates or overwrites.

solid answer

~40 s

k6 calls `handleSummary(data)` once at the very end of a run, after `teardown()`, and expects a map back in the form `{ destination: content }`. Two keys are reserved: `stdout` and `stderr` go to those streams. Every other key is taken as a relative or absolute file path, which k6 opens create-and-truncate — it overwrites, it never appends. Each value must be a `string` or an `ArrayBuffer`; anything else is rejected with `invalid type ..., it needs to be a string or ArrayBuffer`. One return can carry several entries, so the same run can print a one-line note to `stdout` and write `junit.xml`. k6 produces no result file of its own, so a run whose hook returns no file path saved nothing.

code

javascript · 12 lines
javascript
import { jUnit } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';

export const options = { vus: 5, iterations: 10 };

export default function () {}

export function handleSummary(data) {
  return {
    'junit.xml': jUnit(data),
    stdout: 'summary written to junit.xml\n',
  };
}

go deeper

for a junior

Remember the shape: return an object, keys are destinations, values are text or bytes. Know that stdout and stderr are the two reserved keys and that everything else is treated as a file path.

for a middle

Explain that a path key is opened create-and-truncate so each run overwrites, that only a string or an ArrayBuffer is accepted as a value, and that k6 never serialises an object for you.

for a senior

Show that you treat the hook as the only thing that turns a k6 run into an artifact, and that you verify the written path and its permissions rather than assuming a file appeared.

for a principal

Weigh one house convention for what every script returns against per-team formats: the hook lives in the script, so its output travels wherever that script is run.

## The hook and when it runs `handleSummary(data)` is an optional function a k6 script exports. k6 calls it **exactly once per run** — however many VUs, scenarios or iterations the test used — as the last thing the script does: `setup()`, then the scenario functions, then `teardown()`, then `handleSummary()`. k6 runs the call in a **freshly created VU**, which means the script's init context is executed again for that VU. The `data` argument is therefore the only channel carrying the run's aggregated figures into the function; values a module accumulated during the test are not visible there. Exporting the function takes over the end-of-test report. k6 no longer prints its own text block; it prints, writes or discards exactly what you hand back. ## The contract: a map of destination to content k6 expects an object of the form `{ key1: value1, key2: value2, ... }`, and it reads that object literally: - **Keys are destinations, not labels.** The key string decides where the bytes land — nothing else in the return value influences it. - **Values are the payload.** A `string` or an `ArrayBuffer`. k6 does **not** serialise objects for you; if you want JSON you call `JSON.stringify(data)` yourself. - **The map may hold as many entries as you like**, and k6 writes every one of them, so a single run can emit a console line, a JSON file and an XML file together. - **An entry of the wrong type fails that entry**, with `error handling summary object <key>: invalid type ..., it needs to be a string or ArrayBuffer`. - **A non-map return is rejected** with `handleSummary() should return a map with string keys`. - **Returning no file key saves no file.** k6 has no result file it writes on its own at the end of a run. ## Where each key goes | key in the returned object | what k6 does with the value | |---|---| | `stdout` | writes it to the process's standard output | | `stderr` | writes it to the process's standard error | | any other string, e.g. `junit.xml` | opens that path with create + write + truncate and writes the value | A path key may be relative or absolute; a relative one resolves against the working directory k6 was started in. k6 only opens the path — it does not build directories along the way, so a missing parent directory or an unwritable location surfaces as `could not open '<path>'`, grouped under `Could not save some summary information:`. ## Worked example: emitting a JUnit-shaped file ```javascript import { jUnit } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; export const options = { vus: 5, iterations: 10 }; export default function () {} export function handleSummary(data) { return { 'junit.xml': jUnit(data), stdout: 'summary written to junit.xml\n', }; } ``` The helper turns the summary object into an XML `<testsuites>` document; the **key** `junit.xml` is what decides that the document becomes a file with that name. There is no built-in XML writer and no `--out junit` target in k6 — the hook is the whole mechanism. ## The shape of the `data` argument `data` is the aggregated end-of-test object. Which schema you receive is a run-time choice: the `--new-machine-readable-summary` flag, or the `K6_NEW_MACHINE_READABLE_SUMMARY` environment variable, switches k6 to its published machine-readable summary schema for that argument. The **return contract is unaffected** either way — it is still a map of destination to string-or-ArrayBuffer. ## What k6 does not do for you - **It does not write a results file by default.** Nothing at the end of a k6 run leaves an artifact behind unless a key in this map asks for one (or the older `--summary-export=<file>` flag is set on the command line). - **It does not serialise.** An object handed back under a `.json` key is a type error, not a JSON file. - **It does not build directories.** Only the file itself is created, at the path exactly as you spelled it. - **It does not append or rotate.** Each run truncates whatever was at that path and writes over it. - **It does not validate the format against the key.** A key called `junit.xml` holding plain text is written happily — the key is a filename, not a declaration of content type. ## Mistakes that cost people a build 1. Returning the report **as a string** instead of a map. k6 rejects the value and you get no file. 2. Expecting k6 to **append** across runs. The path is opened truncating, so each run replaces the previous file. 3. Assuming a **results file exists anyway** and only the format needed customising. Without a path key, nothing is written. 4. Handing back a plain object for a `.json` key and expecting k6 to stringify it.

  • Can one `handleSummary()` call write a file and print to the console at the same time?
    Yes. The returned map may hold any number of entries and k6 processes all of them, so `{ stdout: line, 'junit.xml': xml }` prints to the terminal and writes the file in the same call.
  • What decides the schema of the `data` object k6 passes to `handleSummary()`?
    The `--new-machine-readable-summary` flag, or `K6_NEW_MACHINE_READABLE_SUMMARY`, switches the argument to k6's machine-readable summary schema. Without it you get the long-standing summary object. The return contract is the same either way.
  • Does k6 create the directory named in a summary file path?
    No. k6 opens the path with create-and-truncate only. A missing parent directory or an unwritable location fails that entry and k6 logs `could not open '<path>'`, while the run's exit code stays as it was.

The returned object is a distribution list rather than a letter. Each key is an address, and k6 delivers that entry's content to it — remove an address and nobody gets a copy.

saying these in an interview costs you the question

  • Thinks k6 writes a results file on its own without the hook
  • Returns a formatted report string instead of a map
  • Expects k6 to append to an existing summary file
  • Believes a --out junit target exists in k6
  • Assumes k6 JSON-serialises a returned object automatically
open as a page

Which statistics can k6's summaryTrendStats option list, and what does it print by default?

level: middleimportance: must knowfreq 58%

basics

~10 s

k6's summaryTrendStats accepts avg, min, med, max, count and p(N) for N between 0 and 100. It defaults to avg, min, med, max, p(90), p(95), and any list you set replaces that default entirely.

open as a page

What does k6's summaryTimeUnit option change in the end-of-test summary?

level: juniorimportance: should knowfreq 40%

basics

~20 s

k6's summaryTimeUnit fixes the unit every duration is printed in throughout the end-of-test summary. It accepts only s, ms or us; left unset, k6 picks a readable unit per value instead, so columns may mix ns, us, ms and s.

open as a page

What happens to k6's default end-of-test summary when a script exports `handleSummary()`?

level: middleimportance: should knowfreq 58%

basics

~20 s

k6 stops printing it. The default text block is only what k6 emits when no hook is exported, so an exported handleSummary replaces it entirely, and returning an empty object leaves the run with no report at all.

open as a page

How does k6 arrive at the p(95) figure it prints for a Trend metric?

level: middleimportance: should knowfreq 47%

basics

~20 s

k6's Trend sink keeps every value the run recorded. For p(95) it sorts them, computes the rank 0.95 * (n - 1), and returns the value at that index, interpolating linearly between the two values that straddle it.

open as a page

A k6 run's `handleSummary()` should write junit.xml, but the CI job finds no file. What do you check?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Check whether the run used --summary-mode=disabled, which skips the hook entirely; whether the hook threw, since k6 only logs that and prints the default block; and whether the returned path was writable from the run's working directory.

open as a page

What does k6's `handleSummaryTimeout` option control, and how do you set it?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

It caps how long k6 lets the handleSummary() call run before interrupting it, and the default is 120s. Set it in the script's exported options object or with the K6_HANDLE_SUMMARY_TIMEOUT environment variable; there is no command-line flag.

open as a page

In k6, what changes when you add p(99) to summaryTrendStats, and what does not?

level: seniorimportance: nice to knowfreq 34%

basics

~10 s

Adding p(99) adds one column to every Trend row in k6's end-of-test summary and nothing else. It creates no threshold, leaves the verdict and exit code untouched, and costs no extra memory.

open as a page

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

level: principalimportance: nice to knowfreq 28%

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.

open as a page