In k6, what exactly does a VU receive when it reads an element from a SharedArray?
answer
- a copy, never a reference
- parsed again on every read
- identity differs between two reads
- the JSON round trip drops functions
basics
~20 sA fresh copy, parsed from that element's stored JSON text on every read and then deep-frozen. Two reads of the same index give two different objects, and anything JSON cannot express does not survive the round trip.
solid answer
~30 sk6 stores each `SharedArray` element as a JSON string, so an index read is a `JSON.parse` of that one string followed by a deep freeze of the result. Nothing is cached: `rows[0] === rows[0]` is `false`, because each read builds a new value. The round trip is also lossy in the way `JSON.stringify` is lossy — a method on a row disappears, a `Date` comes back as an ISO string, a property set to `undefined` is gone, and a class instance arrives as a plain object without its prototype. An index past the end returns `undefined` rather than throwing.
code
javascript · 13 linesimport { SharedArray } from 'k6/data';
const rows = new SharedArray('rows', () => [
{ id: 1, createdAt: new Date(0), ping: () => 'pong', note: undefined },
]);
export default function () {
console.log(rows[0] === rows[0]); // false - a fresh copy on each read
console.log(typeof rows[0].createdAt); // string - the Date did not survive
console.log(rows[0].ping); // undefined - functions are dropped
console.log('note' in rows[0]); // false - the key is gone
console.log(rows[9]); // undefined - out of range, no throw
}go deeper
Treat each read as producing a brand-new plain object. Compare rows by a field such as an id rather than with ===, and expect only data inside them, never methods.
Describe the three steps k6 performs on every read: look up that element's stored JSON string, parse it, deep-freeze the result. Nothing is cached between reads.
Budget for the parse cost when an iteration touches many rows, and keep the stored row small, because the cost is proportional to the element rather than to the array.
Decide what belongs in shared test data at all. Anything with behaviour, identity or a live type cannot survive k6's JSON storage, so those concerns belong in script code rather than in the data set.
## The read path, step by step A `SharedArray` does not hold JavaScript values. It holds one JSON string per element, stored once for the whole k6 process. When a VU evaluates `rows[i]`, k6: 1. Looks up the stored string for index `i`. 2. Runs `JSON.parse()` on that one string — not on the whole array. 3. Deep-freezes the result, including nested objects and nested arrays. 4. Returns it. Nothing is cached, so the next read of the same index repeats steps 1 to 3. An index outside `0 .. length - 1` short-circuits the whole path and returns `undefined`; it does not throw. ## Identity is not stable Because step 2 builds a new value every time, two reads of the same index produce two distinct objects: ```javascript rows[0] === rows[0]; // false ``` That has two practical consequences. First, never compare rows with `===`; compare a field such as an id. Second, if an iteration needs the same row more than once, read it into a local variable and reuse that variable — every fresh `rows[i]` is another parse. ## What the JSON round trip loses The stored form is `JSON.stringify()` output, so whatever that function cannot express never reaches the VU. This is silent: no warning is emitted when the loader returns something unserialisable. | value put into the loader's array | what a VU reads back | |---|---| | `{ id: 1, name: 'ana' }` | the same plain object, deep-frozen | | `{ ping: () => 'pong' }` | `{}` — the function-valued property is dropped | | a `Date` on a row | an ISO-8601 **string**, not a `Date` | | `{ note: undefined }` | `{}` — the key is gone entirely | | an instance of a class | a plain object; the prototype and its methods are gone | | a nested array of numbers | a nested array of numbers, also frozen | The design rule that follows: put **data** in a `SharedArray` and keep **behaviour** in script functions. If a row carries a timestamp you need as a date, store the string and rebuild it in the iteration with `new Date(row.createdAt)`. ## Why the parse is per element, not per array A frequent misreading is that the first access parses the whole stored set and later accesses are cheap. It does not work that way: k6 stores each element as its own string precisely so that a read touches exactly one of them. Indexing a 50,000-row list parses one row's worth of JSON, not 50,000 rows' worth. That is what makes a single index read per iteration affordable, and it is also why row **size** matters more than row **count** — the cost you pay per read is proportional to the element, while the memory you save is proportional to the whole set multiplied by the VU count. ## Why k6 freezes the copy Freezing is not about protecting the shared strings — a write to a copy could never reach them anyway, since the next read re-parses from storage. It is about making the read-only contract visible at the element level. Without it, a VU could mutate a row, observe the change within one expression, and reasonably conclude it had changed shared state. With the freeze, the mistake surfaces at once instead of producing a test that quietly measures the wrong thing. Primitives are the exception: strings, numbers, booleans and `null` are returned unfrozen, because they cannot be mutated to begin with. ## Practical consequences - **Copy to mutate.** `const user = { ...rows[i], token: fresh }` gives an ordinary writable object built from a frozen one. - **Read once per iteration** when you can; the parse cost is per read, not per run. - **Keep rows small.** The parse cost is proportional to the size of the individual element, which is why one giant object wrapped in a one-element array is the worst possible shape for this type. - **Expect no validation.** A row that fails to survive the round trip produces no warning at all, so a missing method surfaces as a `TypeError` at call time in the middle of a run. - **Expect plain data.** Design the stored rows as if they were being sent over the wire, because in effect they are.
- Does k6 cache the parsed element of a SharedArray between reads?No. Every index read re-runs `JSON.parse` on that element's stored string and freezes the result again. If an iteration needs the same row several times, assign it to a local variable once and reuse that variable instead of indexing repeatedly.
- How do you get a mutable row out of a k6 SharedArray?Copy it. `const row = { ...rows[i] }` produces an ordinary, writable object, and a nested field needs its own copy since the freeze is deep. The original stays frozen, and nothing done to the copy can reach the shared storage or any other VU.
saying these in an interview costs you the question
- Expects two reads of the same index to be the same object
- Stores class instances with methods in a SharedArray
- Assumes Date values survive the SharedArray round trip
- Thinks an out-of-range index throws instead of returning undefined
- Believes k6 caches the parsed element after the first read