Skip to the solution
HardSystem Design5 min read

How does SharedArrayBuffer let two Web Workers share memory directly, and what race conditions do you need to guard against?

1.0k views

The problem

Question presented to candidate: "Normally, when a Web Worker and the main thread talk via postMessage, the data gets copied β€” each side has its own separate version. How would you let two workers genuinely share the SAME block of memory, so a write on one side is immediately visible on the other without any copying or messaging round trip? And once they can both touch the same memory, what specifically goes wrong if you are not careful?"

What a strong answer should cover:

  • SharedArrayBuffer allocates a raw binary buffer whose memory is genuinely shared (the same physical bytes) across the main thread and any worker it is handed to β€” unlike a regular ArrayBuffer sent through postMessage, which is either copied (structured clone) or transferred (moved, single-owner) but never simultaneously owned by both sides.
  • A typed array (Int32Array, Float64Array, etc.) is used as a view onto that shared memory β€” the SharedArrayBuffer itself is just raw bytes; reading and writing happens through the view.
  • Because two threads can now touch the same memory at the same instant, a plain value = value + 1 on a shared cell is NOT atomic β€” it is a real read-modify-write sequence with a gap in the middle where another thread's write can be silently lost. This is a genuine race condition, not a theoretical one.
  • Atomics (Atomics.add, Atomics.load, Atomics.store, Atomics.compareExchange) provides real atomic read-modify-write operations that close that gap, plus Atomics.wait/Atomics.notify for actual thread synchronization (blocking a worker until signaled).
  • A candidate should name the real-world browser restriction: SharedArrayBuffer requires a cross-origin-isolated page (COOP/COEP response headers) since the 2018 Spectre-driven disable β€” it is not simply "available" on every page the way ArrayBuffer is.
  • A strong answer distinguishes this from Node.js worker_threads, which exposes the identical SharedArrayBuffer/Atomics mechanism without that browser header requirement, since Node has no cross-origin page-isolation model to protect.

Clarifying questions expected:

  • "Is this for a real browser deployment, where the cross-origin-isolation header requirement genuinely applies, or for a Node.js worker_threads context, where it does not?" β€” changes whether COOP/COEP setup is actually part of the real answer.
  • "Does the shared data need to grow, or is a fixed-size buffer acceptable?" β€” affects whether a growable SharedArrayBuffer (a newer, distinct capability) is relevant.

Code / implementation expected: Yes β€” a real, runnable demonstration of the race (a lost-update count) and the Atomics-based fix (zero lost updates) is the concrete way to prove the concept, not just describe it.

  • workers
  • tricky
  • real-world

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

5 min read Β· 8 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

JavaScriptA real, runnable browser demo: the same race, with a graceful fallback where SharedArrayBuffer is unavailable (verified live)
JavaScriptReference: the actual worker_threads script executed to produce this doc's headline numbers (run directly with node file.js)
// main.js -- run with: node main.js
const { Worker, isMainThread, workerData, parentPort } = require("worker_threads");

if (isMainThread) {
  async function run(useAtomics) {
    const sab = new SharedArrayBuffer(8);
    const data = new Int32Array(sab); // data[0] = counter, data[1] = start gate
    const INCREMENTS_PER_WORKER = 2_000_000;
    const NUM_WORKERS = 8;

    const ready = [];
    const workers = [];
    for (let i = 0; i < NUM_WORKERS; i++) {
      let resolveReady;
      ready.push(new Promise((r) => (resolveReady = r)));
      workers.push(
        new Promise((resolve) => {
          const w = new Worker(__filename, { workerData: { sab, count: INCREMENTS_PER_WORKER, useAtomics } });
          w.on("message", (msg) => (msg === "ready" ? resolveReady() : resolve()));
        })
      );
    }
    await Promise.all(ready);
    Atomics.store(data, 1, 1);
    Atomics.notify(data, 1);
    await Promise.all(workers);
    return { final: data[0], expected: INCREMENTS_PER_WORKER * NUM_WORKERS };
  }

  (async () => {
    const race = await run(false);
    console.log("WITHOUT Atomics: final=" + race.final + " expected=" + race.expected + " lost=" + (race.expected - race.final));
    const safe = await run(true);
    console.log("WITH Atomics.add: final=" + safe.final + " expected=" + safe.expected + " lost=" + (safe.expected - safe.final));
  })();
} else {
  const { sab, count, useAtomics } = workerData;
  const data = new Int32Array(sab);
  parentPort.postMessage("ready");
  Atomics.wait(data, 1, 0);
  for (let i = 0; i < count; i++) {
    if (useAtomics) Atomics.add(data, 0, 1);
    else data[0] = data[0] + 1;
  }
  parentPort.postMessage("done");
}

// REAL captured output from this exact script:
// WITHOUT Atomics: final=4945559 expected=16000000 lost=11054441
// WITH Atomics.add: final=16000000 expected=16000000 lost=0

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 129 of 165 in JavaScript

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you make a custom class iterable with for...of by implementing Symbol.iterator as a generator method?

Continue