SharedArray for Memory Efficiency

8 min read

The previous lesson showed that open() reads a file once per VU. With 500 VUs loading a 10,000-row user dataset, K6 holds 500 copies of that dataset in memory simultaneously. SharedArray solves this: the data is loaded once and shared across all VUs, regardless of VU count.

The memory difference

Regular array vs SharedArray memory usage

Regular array (open + JSON.parse)

  • open() runs once per VU in init

  • 100 VUs × 10,000 records = 1 million records in memory

  • Memory scales linearly with VU count

  • A 5MB file with 500 VUs = 2.5GB memory allocation

  • Serialised separately for each VU's JavaScript context

SharedArray

  • Loader function runs exactly once, regardless of VU count

  • 10,000 records in memory — always, regardless of VU count

  • Memory stays constant as VU count increases

  • 500 VUs, 5MB file → still ~5MB for the data

  • Shared read-only access across all VU JavaScript contexts

Creating a SharedArray

import { SharedArray } from 'k6/data';
 
// The constructor takes: a name (string) and a loader function
// The loader function runs ONCE — its return value is shared across all VUs
const users = new SharedArray('users', function () {
  return JSON.parse(open('./users.json'));
});
 
export const options = { vus: 100, duration: '2m' };
 
export default function () {
  // Access like a regular array — random element
  const user = users[Math.floor(Math.random() * users.length)];
 
  // user.email, user.password, etc. are available
  http.post('https://api.example.com/login',
    JSON.stringify({ email: user.email, password: user.password }),
    { headers: { 'Content-Type': 'application/json' } }
  );
}

The name 'users' is a label used in K6's debug output. It must be unique if you create multiple SharedArrays in the same script.

SharedArray with CSV

Combine SharedArray with papaparse for memory-efficient CSV loading:

import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
 
const users = new SharedArray('users', function () {
  return papaparse.parse(open('./users.csv'), {
    header: true,
    skipEmptyLines: true,
  }).data;
});
 
export default function () {
  const user = users[Math.floor(Math.random() * users.length)];
  // ...
}

Indexing patterns

SharedArrays support the same indexing patterns as regular arrays. Choose based on what your test scenario requires:

// Random — different user each iteration, no coordination needed
const user = users[Math.floor(Math.random() * users.length)];
 
// Per-VU — each VU always uses the same user
// Good for simulating persistent sessions: VU 1 is always Alice, VU 2 is always Bob
const user = users[(__VU - 1) % users.length];
 
// Per-iteration — sequential cycling within each VU
// VU 1 uses users[0] on iter 0, users[1] on iter 1, etc.
const user = users[__ITER % users.length];
 
// Global unique — each iteration across all VUs gets a different record
// Works best when total iterations ≤ users.length
const user = users[(__VU - 1 + __ITER * __ENV.VU_COUNT) % users.length];

The per-VU pattern is the most predictable for scenarios requiring user-specific state (login → session → actions — all as the same user).

SharedArray is read-only

SharedArray data cannot be modified during the test. Attempting to assign to an element throws an error:

const users = new SharedArray('users', function () {
  return JSON.parse(open('./users.json'));
});
 
export default function () {
  users[0].email = 'modified@test.com';  // ❌ Throws TypeError: Cannot assign to read-only property
}

This is intentional — the data is shared across VUs, and mutable shared state would require locking that would destroy K6's concurrency model. If you need to track per-VU state (like a session token obtained after login), use a module-level variable that each VU sets independently.

Multiple SharedArrays

A script can have multiple SharedArrays — use distinct names for each:

import { SharedArray } from 'k6/data';
 
const users    = new SharedArray('users',    () => JSON.parse(open('./users.json')));
const products = new SharedArray('products', () => JSON.parse(open('./products.json')));
const promos   = new SharedArray('promos',   () => JSON.parse(open('./promos.json')));
 
export default function () {
  const user    = users[Math.floor(Math.random() * users.length)];
  const product = products[Math.floor(Math.random() * products.length)];
  const promo   = promos[Math.floor(Math.random() * promos.length)];
  // ...
}

Each SharedArray's loader runs once. Memory usage is the sum of all three datasets, not multiplied by VU count.

When not to use SharedArray

SharedArray is for large, read-only reference data — user pools, product catalogues, test payloads. It is not appropriate for:

  • Data generated during setup — the setup() return value handles this (pass via data parameter)
  • Small inline arrays — a 5-element array is fine as a plain const at module level
  • Per-VU mutable state — use a module-level variable that each VU's context manages independently

⚠️ Common mistakes

  • Using a regular const array with open() and wondering why memory is high. const users = JSON.parse(open('./users.json')) copies the data once per VU. Wrap it in new SharedArray(...) to load once.
  • Trying to modify SharedArray elements. SharedArray is immutable by design. Track mutable per-VU state in a separate module-level variable.
  • Using the same name for two SharedArrays. K6 identifies SharedArrays by name. Two arrays with the same name will reference the same underlying data — the second constructor's loader function may not run. Use unique descriptive names.
  • Putting large data generation logic inside the SharedArray loader instead of open(). The loader runs once, which is correct. But if it makes HTTP requests or does expensive computation, it blocks K6's init phase. Keep the loader to: open file → parse → return.

🎯 Practice task

Compare memory usage with and without SharedArray. 30 minutes.

  1. Create products.json with 1,000 product entries: [{ "id": 1, "name": "Product 1", "price": 9.99 }, ...]. You can generate this with a quick Node.js script outside K6 or create a smaller version manually.
  2. Write a K6 script that loads this file with a plain JSON.parse(open('./products.json')) — no SharedArray. Run with vus: 10, duration: '10s'. Note the output (K6 does not show memory directly, but observe startup time).
  3. Refactor to use new SharedArray('products', () => JSON.parse(open('./products.json'))). Run again with the same settings. The test should behave identically but use less memory at high VU counts.
  4. In the default function, select a random product and make a GET to https://httpbin.org/get?productId=${product.id}&name=${encodeURIComponent(product.name)}. Add a check that status is 200.
  5. Change to per-VU selection: products[(__VU - 1) % products.length]. Add a console.log showing the product name. Run with vus: 5, iterations: 5. Confirm VU 1 always logs the same product.
  6. Try assigning to a SharedArray element — products[0].price = 0 — inside the default function. Observe the error K6 throws.

// tip to track lessons you complete and pick up where you left off across devices.