Skip to solution
hardSystem Design

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

1.0k views
01

Understand 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.

workerstrickyreal-world
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for JavaScript concurrency / Web Worker interviews. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every number below is real, measured output — a genuine 4-worker race run a

Solution ready — 2 min read

Classified // press E to declassify

04

Run the code

JSA real, runnable browser demo: the same race, with a graceful fallback where SharedArrayBuffer is unavailable (verified live)
Reference: 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
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 129 of 165 decoded in the JavaScript track. One more won't hurt.

Back to track