What does k6 do to the value setup() returns before the default function and teardown see it?
answer
- it is not the same object
- bytes, not references
- one encode, one decode per consumer
- methods do not survive JSON
- extract inside setup, return primitives
basics
~10 sk6 JSON-encodes the value once and decodes it again for each consumer, so only plain JSON survives: objects, arrays, strings, numbers, booleans and null. Methods, prototypes and functions do not make the trip.
solid answer
~40 sThe value is passed through a JSON encode once, stored on the runner as raw bytes, and JSON-decoded separately for every VU and again for `teardown()`. The consequence is that `data` is always plain JSON data. A k6 `Response` object arrives with its data fields but without its methods; a class instance arrives as a bare object with no prototype; a function has no JSON form and never arrives at all. If the value cannot be encoded, k6 fails the run with `error marshaling setup() data to JSON`. Scalars are legal — `return 42` works as well as an object — and an `async setup()` has its promise resolved before the encode.
code
javascript · 17 linesimport http from 'k6/http';
export function setup() {
const res = http.post('https://api.example.com/login', '{}');
// Wrong: res.json() is a method, and methods do not survive the encode.
// return { session: res };
// Right: extract while the real object is still alive.
return { token: res.json('token'), loginStatus: res.status };
}
export default function (data) {
http.get('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${data.token}` },
});
}go deeper
Remember that what arrives in the data argument is plain JSON, so read simple fields like data.token and do not expect to call methods on it.
Explain the mechanism: one JSON encode into bytes on the runner, then a separate decode per VU and for teardown, which is why methods and prototypes are gone.
Diagnose the failure from the symptom. An iteration dying on data.session.json is not a function is the encode discarding methods, and the fix is to extract the value inside setup.
Treat the encode as the contract between stages. Keeping the payload to primitives is what makes a script portable between a laptop run and a distributed one without a rewrite.
## One encode, many decodes When `setup()` finishes, k6 does not hand the JavaScript value it produced to anyone. It performs a single JSON encode and keeps the resulting **bytes** on the runner. Every consumer then reconstructs its own value from those bytes: - each VU decodes them the first time it runs an iteration; - `teardown()` decodes them once more after all executors have finished. k6 also validates the round trip immediately: it encodes, then decodes its own output, so a value that encodes into something unreadable fails at the setup stage rather than mysteriously later. This design is deliberate. Bytes are the only representation that survives being written to a file, shipped to another machine, or handed to a second process, and k6's stated goal is that the same script runs unchanged locally and distributed. A live JavaScript object graph could not cross those boundaries; a JSON document can. ## What survives, and what does not | What `setup()` returns | What `default(data)` receives | |---|---| | `{ token: 'abc', ttl: 900 }` | the identical plain object | | `42`, `'abc'`, `true`, `[1, 2, 3]` | the same scalar or array — objects are not required | | nothing, or `undefined` | `undefined`, and no bytes are stored at all | | a k6 `Response` from `http.get()` | its data fields, with every method gone | | an instance of your own class | a bare object; the prototype and its methods are gone | | anything with no JSON form, such as a function | never arrives; k6 can fail the run while encoding it | The rule of thumb that covers every row: **if it would not survive being written to a `.json` file and read back, it does not survive `setup()`.** k6's own test suite proves the interesting half of this — it returns an HTTP response, an HTML selection and a cookie jar from `setup()` and then asserts that every *non-function* property comes back intact on the other side. ## The practical trap The trap is that the failure is silent in shape but loud in behaviour. This looks like it should work: - `setup()` returns `{ session: http.post('/login') }`; - the default function calls `data.session.json('token')`. The object is there, the property is there, and the iteration still dies — `json()` is a method, and methods are exactly what the encode discarded. The fix is to do the extraction **inside** `setup()` and return the primitive: `return { token: res.json('token') }`. Pull out what you need while you still hold the real object. The same reasoning rules out returning a helper: `return { auth: () => 'Bearer ' + t }` cannot work, because a function has no JSON representation. Anything callable has to be defined in the module's init scope, where every VU compiles its own copy of the script anyway. ## When the encode fails outright If the returned value cannot be encoded at all, k6 does not quietly substitute a placeholder. `setup()` fails, and the message is `error marshaling setup() data to JSON`. Since a failed setup ends the run before any VU starts, you find out in the first second rather than after five minutes of load. ## Async setup `setup()` may be `async`. k6 resolves the returned promise before it encodes anything, so: ```javascript export async function setup() { const token = await mintToken(); return { token }; } ``` hands `{ token: '...' }` to every VU, not a pending promise. The entire await still has to complete inside `setupTimeout`, which defaults to 60 seconds in k6 v2. ## How to design the return value - Return **primitives and plain containers** — strings, numbers, booleans, arrays and objects of those. - Do the parsing, extracting and formatting in `setup()`, while the rich objects are still alive. - Keep it small: the bytes are decoded once **per VU**, so the payload is held as many times over as you have virtual users. - Never assume identity. Two VUs holding "the same" object hold two decodes of the same bytes, and neither can see the other's writes. ## A self-check before you return Run the returned value past three questions, in this order: 1. **Would it survive `JSON.stringify` followed by `JSON.parse`?** If not, whatever you needed from it has to be pulled out inside `setup()` first. 2. **Does anything downstream call a method on it?** If yes, that call will fail, because the decode produced a plain object with no behaviour attached. 3. **Is it worth its size?** The bytes are decoded once per VU, so an expensive payload is paid for as many times over as you have virtual users. The habit that avoids all three problems is to treat `setup()` as producing a **document**, not an object. Documents are made of strings, numbers, booleans, arrays and nested plain objects; anything with behaviour is a program, and programs live in the module scope where every VU compiles its own copy anyway. Once you hold that distinction, the encode stops being a surprise and becomes the obvious boundary it is: the last point in the test at which live JavaScript exists, and the first point at which everything downstream is data.
- Why does k6 encode the setup value instead of sharing the object?Because bytes cross process and machine boundaries and object graphs do not. The same encoded document can be shipped to every instance of a distributed run, so a script behaves identically on one machine or twenty. Sharing a live object would also need locking across every VU.
- Must a k6 setup() return an object?No. Any JSON-encodable value works, including a bare number or string, and k6's own tests cover `return 42`. An object is only the convention because it lets you add fields later without changing every call site that reads `data`.
saying these in an interview costs you the question
- Thinks every VU shares one live setup object
- Returns a k6 Response and calls its methods in VU code
- Returns a helper function from setup and expects to call it
- Assumes an unencodable value is silently dropped rather than failing setup
- Believes setup must return an object rather than any JSON value