A strong answer
Each Worker in Node.js/Bun is a separate V8 Isolate with its own independent JS heap, united by a single process address space. Instead of $O(N)$ Structured Clone (postMessage(obj)), use: (1) Transferable ArrayBuffer (worker.postMessage(u8.buffer, [u8.buffer])): transfers ownership of the underlying C++ memory block to the worker in $O(1)$ time (~3 µs for 100 MB, 0 bytes copied!) and immediately detaches the sender's buffer (u8.byteLength === 0), preventing data races by single-ownership transfer! (2) SharedArrayBuffer + Atomics: maps the exact same physical RAM into multiple Worker Isolates simultaneously. To build a lock-free ring buffer or shared table safely across x86-64 and ARM64 CPUs, you must read/write control indices (writeIdx, readIdx) using Atomics.load(ctrl, i) and Atomics.store(ctrl, i, val) (which emit hardware Sequential Consistency SeqCst memory barriers so payload writes are visible before the index updates!), and use Atomics.notify + Atomics.wait (on workers) or Atomics.waitAsync (on the main Event Loop thread, so it never blocks the Event Loop!).
In a Single-Producer Single-Consumer (SPSC) ring buffer over a SharedArrayBuffer where Producer writes data[w & mask] then Atomics.store(ctrl, WRITE_IDX, w + 1) and Consumer reads w = Atomics.load(ctrl, WRITE_IDX) before reading data[r & mask], the SeqCst happens-before edge guarantees the Consumer never observes an uninitialized or stale slot on any CPU architecture. Under the ECMAScript Memory Model, Atomics.store (synchronizes-with) Atomics.load on the same SharedArrayBuffer index establishes a global happens-before edge: all memory writes sequenced before Atomics.store(ctrl, WRITE_IDX, w + 1) (namely data[w & mask] = payload) are guaranteed visible before any read sequenced after Atomics.load(ctrl, WRITE_IDX) observes w + 1.
Transferring a 10 MB ArrayBuffer via postMessage(ab, [ab]) takes ~3 µs (0 bytes copied vs ~42,000 µs for Structured Clone), and a lock-free SharedArrayBuffer Atomics SPSC ring push/pop takes ~40 ns. In production systems (Piscina (worker_threads pool for Node.js), Bun Worker IPC, WebAssembly threads (WebAssembly.Memory({ shared: true })), and Cloudflare/Deno multi-isolate shared telemetry rings.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how
worker_threadsIsolates, TransferableArrayBufferownership move, andSharedArrayBuffer+Atomicssynchronization operates in memory step by step rather than giving a vague textbook definition. - States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (Structured Clone (
postMessage(obj)), Transferable (postMessage(ab, [ab]))) with concrete numbers. - Identifies real-world failure modes (Passing large JSON objects or
Uint8Arrays viaworker.postMessage(data)without the secondtransferListargument[data.buffer]) and explains how production systems mitigate them.
Common mistakes
- Treating
worker_threadsIsolates, TransferableArrayBufferownership move, andSharedArrayBuffer+Atomicssynchronization as a black box without explaining the underlying memory or state transitions. - Ignoring worst-case tail latency or memory overhead (Placing
writeIdxatInt32Array[0]andreadIdxatInt32Array[1]in a high-frequencySharedArrayBufferring buffer). - Forgetting boundary conditions and error recovery (Spawning a brand-new
new Worker('./worker.js')per HTTP request instead of maintaining a warm worker pool).