In a k6 script, when does setup() run and how does its return value reach the other stages?
answer
- one stage before the virtual users
- runs once, not once per VU
- its own throwaway VU
- return value is serialised
- JSON copy for default and teardown
basics
~20 sk6 calls setup() once per test run, after the init stage and before any virtual user starts. Whatever it returns is JSON-encoded once and handed to the default function and to teardown() as their first argument.
solid answer
~40 sA k6 test always runs init code, then `setup()`, then VU code, then `teardown()`. `setup()` is an optional exported function that k6 calls **exactly once** for the whole run, in a throwaway VU of its own, before any executor starts a virtual user. Unlike init code it can use the full k6 API, so it can issue HTTP requests. Whatever it returns is serialised to JSON once and kept on the runner; every VU and `teardown()` then get their own decoded copy of it as the function's first argument. A `setup()` with no return statement leaves that argument as `undefined`. If `setup()` throws, the run fails before any VU starts and `teardown()` is never called.
code
javascript · 24 linesimport http from 'k6/http';
export const options = { vus: 10, duration: '30s' };
export function setup() {
const res = http.post(
'https://api.example.com/login',
JSON.stringify({ user: 'loadtest', pass: __ENV.LOAD_PASS }),
{ headers: { 'Content-Type': 'application/json' } },
);
return { token: res.json('token') };
}
export default function (data) {
http.get('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
export function teardown(data) {
http.post('https://api.example.com/logout', null, {
headers: { Authorization: `Bearer ${data.token}` },
});
}go deeper
Recall the order: init, setup, VU code, teardown. Know that setup runs once for the whole k6 test and that its return value shows up as the first argument of the default function.
Explain the mechanics: a transient VU, the full k6 API available there unlike in init code, one JSON encode on the runner, and a separate decode for each VU and for teardown.
Show judgment about what belongs in the stage. One login instead of one per iteration keeps auth traffic out of the measured work, and a setup that throws costs you the whole run before a single VU starts.
Weigh what the stage buys against what it costs a team: a single point of failure ahead of every run, a hard deadline in setupTimeout, and a payload every VU pays memory for.
## Where `setup()` sits in the k6 lifecycle A k6 script always moves through the same four stages, in the same order: 1. **Init context** — everything at module scope. It prepares the script: imports, file loading, the exported `options` object, and the declaration of the lifecycle functions themselves. 2. **`setup()`** — optional, exported, run exactly once for the whole test. 3. **VU code** — the `default` export, or the function a scenario's `exec` key names, run over and over for as long as the options say. 4. **`teardown()`** — optional, exported, run exactly once after every executor has finished. `setup()` and `teardown()` are ordinary exported functions that k6 locates by name. If a script does not export them, k6 simply logs that they are not defined and skips the stage; their absence is never an error, which is why the smallest useful k6 script is a `default` function on its own. ## A virtual user of its own k6 does not borrow one of your load-generating VUs for `setup()`. It builds a **transient VU** for the call — both its local and its global id are `0` — runs the function inside it, and throws it away. The same happens again for `teardown()`. Two consequences follow, and both are asked about: - **The full k6 API is available.** Init code deliberately cannot make HTTP requests, because k6 wants the init stage to be reproducible across runs. `setup()` is real VU code, so `http.post()` and the rest of `k6/http` work there. - **Nothing else leaks out of that VU.** Its cookie jar, its connections and its JavaScript globals die with it. The *only* thing that survives the stage is the value the function returns. ## The handoff, step by step 1. `setup()` returns a value. k6 JSON-encodes it **once** and stores the raw bytes on the runner. 2. Each VU decodes those bytes into its own JavaScript value, lazily, on its first iteration. 3. `teardown()` decodes the same bytes again into yet another value, after all executors are done. 4. Each of those decoded values arrives as the **first positional argument** of the function. Because the channel is one encode followed by independent decodes, what arrives is always plain JSON data — objects, arrays, strings, numbers, booleans and `null`. It also flows one way: nothing a VU writes ever gets back to `setup()`, across to another VU, or forward into `teardown()`. If `setup()` has no `return` statement, k6 stores nothing at all and both `default` and `teardown()` receive `undefined` — not an empty object, and not `null`. A script written as `data.token` will therefore throw a `TypeError` on its very first iteration, which is the single most common way this stage bites a newcomer. An `async setup()` is fine: k6 unwraps the returned promise before encoding it, so `export async function setup() { return await mintToken(); }` hands the resolved value on exactly as a synchronous return would. ## Worked example: minting one auth token for every VU The canonical use of the stage is a single login whose result every VU reuses. Logging in once in `setup()` rather than once per iteration keeps the authentication endpoint out of your measurements and stops fifty VUs racing to create fifty sessions. `setup()` posts the credentials, returns `{ token }`, every VU sends that token as a bearer header, and `teardown()` uses the same token to log the session out again. The `codeExamples` section shows the whole shape. ## Failure modes worth knowing | What happens | Result | |---|---| | `setup()` throws | the run fails, no VU starts, and `teardown()` is **not** called | | `setup()` overruns `setupTimeout` (default `60s`) | k6 exits **100** with `setup() execution timed out after 60 seconds` | | `teardown()` overruns `teardownTimeout` (default `60s`) | k6 exits **101**; the VU work has already happened | | `--no-setup` is passed | the function is skipped entirely and `data` is `undefined` | | the returned value cannot be JSON-encoded | k6 fails with `error marshaling setup() data to JSON` | ## Why the stage exists at all Everything a test needs *once* belongs here: obtaining a credential, creating a fixture record, checking that the target is reachable before you spend five minutes hammering it. Everything a test needs *per VU* or *per iteration* belongs in init code or in the VU function. The split is what lets k6 promise that the same script runs unchanged whether one machine or twenty execute it — the expensive, order-sensitive part is done once, and the cheap, repeatable part is done a million times.
- Can a k6 setup() be declared async?Yes. k6 unwraps the promise before encoding, so `export async function setup() { return await mintToken(); }` hands the resolved value to `default` and `teardown` exactly as a plain return would. The whole await still has to finish inside `setupTimeout`, which defaults to 60 seconds.
- Does k6 still run teardown() when the run was aborted part-way through?Yes. k6 executes `teardown()` on the global context rather than the run context, so a breached threshold, an `exec.test.abort()` call or a single Ctrl+C still lets it finish. A second Ctrl+C kills it, and `--no-teardown` skips it outright.
- What if a k6 script exports teardown() but not setup()?That is legal. k6 logs that `setup()` is not defined, skips the stage, and calls `teardown(undefined)` after the executors finish. Teardown work that does not depend on setup data — deleting a fixture by a known id, posting a webhook — needs no setup function at all.
It is the shift briefing before the doors open: held once, for everyone, before anybody is on the floor. Every worker walks in with the same printed sheet.
saying these in an interview costs you the question
- Thinks setup() runs once per VU rather than once per test
- Believes setup() runs at the start of every iteration
- Says setup() cannot make HTTP requests, confusing it with init code
- Assumes teardown() still runs after setup() has thrown
- Expects an empty object when setup() returns nothing