Skip to the solution
HardSystem Design8 min read

How would you transfer a large ArrayBuffer between a worker and the main thread without copying it, using postMessage's transfer list?

324 views

The problem

Question presented to candidate: "You have a large ArrayBuffer, say a decoded audio buffer or a big binary payload, that a worker needs to process. If you just call postMessage with it, the browser has to structured-clone the whole thing, which means copying every byte. How would you hand it off without paying that copy cost, and what actually happens to your original buffer once you do?"

What a strong answer should cover:

  • By default, an object passed to postMessage is structured-cloned β€” for an ArrayBuffer, that means a real, byte-for-byte copy, which gets expensive for a large buffer.
  • Passing a second argument to postMessage β€” the transfer list, an array containing the specific ArrayBuffer(s) to transfer β€” moves ownership instead of copying: the receiving side gets the real, same underlying memory, and the sending side's buffer is genuinely detached (its byteLength becomes 0) immediately, synchronously, at the postMessage call itself.
  • A precise answer names the real trade-off this creates: the sender can no longer use that buffer at all after transferring it β€” this is a real, one-directional handoff, not a shared view.
  • πŸ“Œ Interview term: SharedArrayBuffer is the deliberate alternative when BOTH sides genuinely need continued access β€” it cannot be placed in a transfer list at all, since it was never single-owner to begin with.
  • A strong answer names what belongs in a transfer list beyond ArrayBuffer: MessagePort is the other classic example, and the list of transferable types has grown over time in both browsers and Node's worker_threads.
  • A strong answer can quantify the real cost difference rather than just asserting "it's faster" β€” a genuine, measured before/after number for a specific buffer size makes the case concrete.

Clarifying questions expected:

  • "Does the main thread still need this data for anything after handing it to the worker, or is the worker now the sole, permanent owner?" β€” if the main thread needs it again afterward, a transfer is the wrong tool and a copy (or SharedArrayBuffer) is genuinely required instead.
  • "Is this a one-off large payload, or an ongoing stream of many smaller buffers?" β€” affects whether the per-call overhead of setting up the transfer list is worth it versus a different pattern.

Code / implementation expected: Yes β€” real, measured proof that the transfer is near-instant and genuinely detaches the sender's buffer, contrasted with a real measured copy cost for the identical data, is the concrete way to demonstrate the mechanism.

  • 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

8 min read Β· 9 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: transferring an 8MB ArrayBuffer to a real Worker (matches this doc's live-verified numbers)
JavaScriptReference: the real Node worker_threads timing comparison that produced this doc's 0.030ms vs 29.957ms numbers (run with node file.js)
const { Worker, isMainThread, parentPort } = require("worker_threads");

if (isMainThread) {
  const SIZE = 64 * 1024 * 1024; // 64MB
  const buf = new ArrayBuffer(SIZE);
  const view = new Uint8Array(buf);
  view[0] = 111;
  view[SIZE - 1] = 222;

  console.log("BEFORE transfer: main byteLength=" + buf.byteLength);

  const w = new Worker(__filename);
  const t0 = process.hrtime.bigint();
  w.postMessage({ buf }, [buf]); // transfer, not copy
  const t1 = process.hrtime.bigint();

  console.log("AFTER postMessage with transfer list: main buf.byteLength=" + buf.byteLength + " (detached=" + (buf.byteLength === 0) + ")");
  console.log("postMessage call itself took " + (Number(t1 - t0) / 1e6).toFixed(3) + "ms for a 64MB buffer");

  w.on("message", (msg) => {
    console.log("worker reports:", msg);
    w.terminate();
  });

  const buf2 = new ArrayBuffer(SIZE);
  const t2 = process.hrtime.bigint();
  const w2 = new Worker(__filename);
  w2.postMessage({ buf: buf2 }); // no transfer list -- structured clone COPY
  const t3 = process.hrtime.bigint();
  console.log("AFTER postMessage WITHOUT transfer list: main buf2.byteLength=" + buf2.byteLength);
  console.log("postMessage (copy) call itself took " + (Number(t3 - t2) / 1e6).toFixed(3) + "ms for a 64MB buffer");
  w2.on("message", () => w2.terminate());
} else {
  parentPort.on("message", (msg) => {
    const tStart = process.hrtime.bigint();
    const arr = new Uint8Array(msg.buf);
    const tEnd = process.hrtime.bigint();
    parentPort.postMessage({
      receivedByteLength: msg.buf.byteLength,
      firstByte: arr[0],
      lastByte: arr[arr.length - 1],
      workerTransferMs: (Number(tEnd - tStart) / 1e6).toFixed(3),
    });
  });
}

// REAL captured output:
// BEFORE transfer: main byteLength=67108864
// AFTER postMessage with transfer list: main buf.byteLength=0 (detached=true)
// postMessage call itself took 0.030ms for a 64MB buffer
// AFTER postMessage WITHOUT transfer list: main buf2.byteLength=67108864
// postMessage (copy) call itself took 29.957ms for a 64MB buffer
// worker reports: { receivedByteLength: 67108864, firstByte: 111, lastByte: 222, workerTransferMs: '0.003' }

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 154 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

What does the 'new' keyword do?

Continue