How would you decide what a k6 setup() should return for a large, long-running test?
answer
- size, freshness, and who runs it
- the cost multiplies by VU count
- one shot, never re-entered
- one instance mints for the fleet
- small, stable, identical for every VU
basics
~20 sReturn the smallest JSON value every VU genuinely needs. k6 decodes it separately for each VU, caps the function at setupTimeout, and never re-runs it, so a large or perishable payload costs memory or goes stale mid-run.
solid answer
~40 sThree k6 mechanics drive the call. First, the value is encoded once but **decoded per VU**, so its memory cost scales with VU count — a big dataset belongs in a `SharedArray`, not in the setup return. Second, `setup()` must finish inside `setupTimeout`, `60s` by default, so minting one token fits and provisioning thousands of accounts does not. Third, the handoff is one-shot and immutable: a credential that expires before the run ends cannot be reissued from `setup()`. And when a run is split across instances, exactly one of them executes `setup()` and the others are handed the same encoded bytes, so anything instance-specific must not be minted there. What is left — small, stable, identical for every VU — is what belongs.
code
javascript · 22 linesimport http from 'k6/http';
import { SharedArray } from 'k6/data';
// Large and read-only: held once for the process, not once per VU.
const users = new SharedArray('users', () =>
Array.from({ length: 10000 }, (_, i) => ({ name: `user${i}` })),
);
export const options = { vus: 500, duration: '2h', setupTimeout: '2m' };
export function setup() {
// Small, stable, identical for every VU - and cheap for the deadline.
const res = http.post('https://api.example.com/login', '{}');
return { token: res.json('token'), runId: `run-${Date.now()}` };
}
export default function (data) {
const user = users[Math.floor(Math.random() * users.length)];
http.get(`https://api.example.com/orders?user=${user.name}`, {
headers: { Authorization: `Bearer ${data.token}`, 'X-Run': data.runId },
});
}go deeper
Keep the return value small and simple - a token, an id, a base URL. Anything bigger or more complicated usually belongs somewhere other than the setup stage.
Explain why the size matters: k6 decodes the value once per VU, so the memory cost multiplies, and a large read-only dataset belongs in a SharedArray instead.
Reason about staleness. A credential that expires inside the run window cannot be reissued from setup, and refreshing in VU code adds one refresh per VU to traffic you did not plan for.
Decide how much the run is allowed to create at all. Provisioning inside setup puts a single point of failure in a 60-second window ahead of every test, with nothing measured when it fails.
## Three k6 constraints decide it There is no general rule about what a test should provision; there is a very specific rule about what k6's setup **channel** can carry well. Four mechanics bound the decision: 1. **Cost scales with VU count.** k6 encodes the value once, but each VU decodes its own copy and holds it for the whole run. 2. **The stage has a hard deadline.** `setupTimeout` defaults to `60s`, and it is validated to be positive — there is no unlimited mode. 3. **The handoff is one-shot and immutable.** k6 never re-enters `setup()`, and nothing a VU writes propagates anywhere. 4. **Exactly one instance runs it.** When a test is split across instances, one executes `setup()` and every other receives the same encoded bytes. Anything that fights one of those four is in the wrong place, however sensible it looks as test design. ## Sizing: the payload is held once per VU A 2 MB array returned from `setup()` in a 500-VU test is not 2 MB of memory on the load generator; it is roughly 500 copies of it, because each VU decodes the same bytes into its own JavaScript value. Load generators that run out of memory part-way through a ramp are frequently doing exactly this. - **Small and identical for every VU** — a token, a base URL, a tenant id, a run correlation id. This is what the channel is for. - **Large and read-only** — a fixture list of ten thousand users. This is what `SharedArray` exists for; it is loaded in init code and held once for the whole process rather than once per VU. - **Large and per-VU** — usually a sign the data should be generated from the VU's own identity instead of shipped to it. The test is blunt: multiply the encoded size by your peak VU count and ask whether the answer is a number you would be comfortable seeing in the generator's resident memory. ## Perishability: the channel cannot be refreshed `setup()` runs once and never again. A token with a fifteen-minute lifetime handed to a two-hour run will expire mid-flight, and there is no mechanism in the stage to reissue it. What actually happens is worse than a clean failure: each VU independently notices its `401`, and if the script refreshes in VU code, the refresh happens once **per VU**, adding traffic you did not model. Worse still, `teardown()` decodes the original bytes, so it attempts its cleanup with the stale credential. The options, in order of preference: 1. Obtain a credential whose lifetime comfortably exceeds the planned run. 2. Refresh in VU code, accept the per-VU cost, and make sure `teardown()` does not depend on the value still being valid. 3. Move the credential out of the setup channel entirely and read it from `__ENV`, which every VU can see without an encode. ## Distribution: one instance mints, the rest receive When a k6 test is split across instances, the setup stage is coordinated: one instance actually calls `setup()` and the others are handed the JSON it produced. That is what makes the value genuinely uniform across a fleet — but it also means anything derived from *where* `setup()` ran is wrong for everybody else. A source IP, a local file handle, or a per-instance connection cannot be minted there. ## The checklist | Question | If the answer is no | |---|---| | Is it identical for every VU? | derive it per VU instead, from the VU's identity | | Does it survive a JSON encode? | extract the primitive you need inside `setup()` | | Is `size × peak VUs` acceptable? | move it to a `SharedArray` in init code | | Does it stay valid for the whole run? | lengthen its lifetime, or refresh in VU code | | Can it be produced within `setupTimeout`? | provision it before the run, outside k6 | | Is it independent of which machine ran setup? | it cannot come from this stage at all | ## The organisational angle The last decision is one a lead owns rather than an individual script author: **how much of the environment is a k6 run allowed to create?** A setup that provisions is convenient and self-contained, but it puts a single-point-of-failure inside a 60-second window ahead of every run, and an exit code 100 there costs the whole run with nothing measured. Teams that run performance tests continuously usually converge on provisioning ahead of the run and keeping `setup()` to a credential fetch and a reachability check — cheap, fast, and unlikely to be what fails.
- Why is a big dataset worse in a k6 setup() return than in a SharedArray?Because the setup value is decoded separately by every VU, so its memory cost multiplies by the VU count. A SharedArray is loaded once in init code and held once for the process, which is why it exists as a distinct mechanism.
- What does the setupTimeout default tell you about how much work belongs in a k6 setup()?That the stage is designed for seconds of work, not minutes. A 60-second budget comfortably covers a login and a reachability check; bulk provisioning has to move ahead of the run, where a failure does not cost you the whole test.
- How do you keep a k6 script runnable when setup() is skipped?Guard the argument in VU code and fall back to `__ENV`, so `--no-setup` degrades to reading a credential from the environment rather than throwing a TypeError. That keeps quick local runs against an already-seeded environment cheap.
saying these in an interview costs you the question
- Returns a large dataset from setup instead of using SharedArray
- Assumes k6 re-runs setup when the credential expires
- Ignores that the payload is decoded once per VU
- Provisions thousands of records inside the 60-second budget
- Mints instance-specific state in setup for a distributed run
- Expects teardown to see a credential a VU refreshed