What happens in k6 when a script assigns into a SharedArray or calls push() on it?
answer
- read-only for the whole run
- every write path throws
- a TypeError, not a silent no-op
- returned elements are frozen too
- not a channel between VUs
basics
~20 sA k6 SharedArray is read-only: assigning to an index, calling push, or calling sort all raise TypeError: SharedArray is immutable. Elements read out of it come back deep-frozen, so a SharedArray cannot carry data between VUs.
solid answer
~30 sEvery write path into a `SharedArray` is closed. `rows[0] = user`, `rows.push(user)`, `rows.sort(fn)` and `rows.length = 0` all end up at the same guard and throw `TypeError: SharedArray is immutable`; setting a non-index property such as `rows.owner = 'qa'` throws a different message about a dynamic array. Elements are protected too: k6 deep-freezes each object it returns, so writing to a field of `rows[0]` is rejected as well. The design consequence matters more than the errors — a `SharedArray` is not a channel, so it cannot be used as a pool of accounts that VUs consume.
code
javascript · 14 linesimport { SharedArray } from 'k6/data';
const rows = new SharedArray('rows', () => [{ id: 1 }, { id: 2 }]);
export default function () {
try {
rows[0] = { id: 99 };
} catch (e) {
console.log(e.toString()); // TypeError: SharedArray is immutable
}
const mine = { ...rows[0], id: 99 }; // copy first, then change the copy
console.log(mine.id, rows[0].id); // 99 1
}go deeper
Remember the one-line rule: once a k6 SharedArray is built it can only be read. If a row needs changing, copy it into a new object first and change the copy.
Explain that index assignment, push, sort and setting length all reach the same guard and throw TypeError: SharedArray is immutable, and that the elements handed back are deep-frozen as well.
Spot the design mistake in review. Any plan where VUs consume rows from a shared pool cannot work with a SharedArray, and it fails the first time an iteration tries to write.
Decide where shared mutable state should live at all. k6 deliberately offers no cross-VU write path, so anything that must be coordinated between VUs belongs behind a service the test calls.
## Every write path is closed A `SharedArray` from `k6/data` is read-only from the moment its loader returns until the run ends. That is enforced at the array itself, not by convention: k6 wraps the shared data in an array-like object whose write hooks unconditionally raise a `TypeError` with the message `SharedArray is immutable`. Because the ordinary array methods are implemented in terms of those same hooks, the ones that write hit the guard too: - `rows[3] = user` — a direct index write. - `rows.push(user)` — writes at index `length`, so it throws exactly like an index write. - `rows.sort(byName)` — writes the reordered values back into the array. - `rows.length = 0` — changing the length is a write as well. The one different message is for a **non-index** property: `rows.owner = 'qa'` reports `Cannot set property "owner" on a dynamic array`, because the wrapper accepts no own properties other than `length`. ## Why there is no writable storage to write to The shared copy is not a JavaScript array at all. k6 keeps one JSON string per element, held once for the whole k6 process, and reconstructs an element only when a VU asks for it. There is nowhere for a write to land: a mutation would have to be re-serialised into the shared strings and made visible to every other VU's runtime, which is exactly the cross-VU coordination k6 deliberately does not offer. It is worth being precise about what is rejected. The guard sits on the wrapper's write hooks, so it fires for every route into the array, whether the script wrote an index itself or a built-in method wrote one on the script's behalf. There is no strict-mode escape hatch and no option that relaxes it. ## The elements are frozen too Closing the array would not be enough on its own, because a VU could take an element out and change it. So every value k6 returns from an index read is **deeply frozen** — the object, its nested objects and its nested arrays. Adding a property to one under strict mode fails with a `TypeError` such as `Cannot add property data, object is not extensible`. Primitive elements (strings, numbers, booleans, `null`) are returned as they are, since they cannot be mutated in the first place. Even without the freeze the write would be pointless: the next `rows[0]` re-parses the element from its stored string, so a change to a copy could never be observed again, by that VU or any other. ## What each expression actually does | expression, where `rows` is a k6 `SharedArray` | result | |---|---| | `rows.length` | the element count | | `rows[3]` | a fresh, deep-frozen copy of element 3 | | `rows[3] = user` | `TypeError: SharedArray is immutable` | | `rows.push(user)` | `TypeError: SharedArray is immutable` | | `rows.sort(byName)` | `TypeError: SharedArray is immutable` | | `rows.length = 0` | `TypeError: SharedArray is immutable` | | `rows.owner = 'qa'` | `TypeError: Cannot set property "owner" on a dynamic array` | | `rows.filter(fn)` | a new ordinary array — no error, and no sharing either | That last row is the trap worth remembering: the read-only methods do not throw, they quietly build a private copy. ## The consequence: a SharedArray is not a channel The most common design mistake is to reach for a `SharedArray` as a pool of one-use test accounts that VUs pop from. It cannot work — there is no write path, so no VU can mark a row as taken, and no VU could see another's bookkeeping even if it could write it. In k6, the idiomatic answer is to compute a deterministic index from the execution counters the runtime exposes, so each iteration lands on a row of its own without any shared mutable state. The same reasoning rules out the softer variants. You cannot shuffle the array in place before the run starts using it, and you cannot append a row computed later. Anything that has to vary must be computed on the read side, from the index you choose. ## What you can still do 1. **Read freely**: `length`, index access, `for...of`, `forEach`, `find`, `indexOf` and `slice` all work. 2. **Copy before changing**: `const user = { ...rows[i], token: fresh }` gives you an ordinary, writable object; the frozen original is untouched. 3. **Shape the data in the loader**, where it runs once, instead of trying to reshape it in place afterwards.
- Can a k6 SharedArray hand unused test accounts from one VU to another?No. It is read-only for the whole run and every write path throws, so no VU can mark a row consumed, and separate VU runtimes could not observe the bookkeeping anyway. The k6 answer is to derive a row index from the execution counters the runtime exposes.
- In k6, does writing to a field of an element read from a SharedArray change the shared data?No. k6 deep-freezes every object it returns, so the write is rejected, and even if it were not, the next read re-parses that element from its stored JSON string. Spread the element into a new object first and change that copy.
saying these in an interview costs you the question
- Thinks a SharedArray can queue accounts for other VUs to consume
- Expects push on a SharedArray to grow that VU's own view
- Believes writes to a SharedArray are silently ignored, not thrown
- Mutates an element in place and expects other VUs to see it
- Calls sort on a SharedArray to order rows inside an iteration