skip to content

You are designing a browser worker pool that processes large binary frames. How do you decide between transferring ArrayBuffers back and forth and putting the data in a SharedArrayBuffer that all the workers read?

level: principalimportance: nice to knowfreq 22%

answer

  1. Ownership, not speed, decides
  2. Detachment makes races impossible
  3. Shared memory drags in the headers
  4. Atomics is a bug class you adopt
  5. Pool and ping-pong the buffers

basics

~20 s

Prefer transfer: it is zero-copy, needs no special headers, and gives exclusive ownership so no synchronization is possible to get wrong. Choose SharedArrayBuffer only when several agents must read or write the same bytes concurrently — and accept cross-origin isolation plus atomics as the price.

solid answer

~50 s

Start from ownership. If each frame is handled by exactly one worker at a time, transferring the `ArrayBuffer` is already zero-copy, and because the buffer detaches on send, only one agent can touch it — the data race is structurally impossible. That needs no headers, no `Atomics`, and no third-party negotiation. Reach for `SharedArrayBuffer` only when the access pattern is genuinely concurrent: several workers reading one large asset at once, or a ring buffer with a producer and consumers running in parallel. The bill is real — the whole document must be cross-origin isolated with COOP and COEP, which breaks third-party embeds and popup opener flows, and every concurrent access has to be coordinated through `Atomics`, with bugs that will not reproduce on demand. My default is transfer with a pooled set of buffers ping-ponged between page and workers, and shared memory only where measurement shows transfer cannot express the access pattern.

go deeper

for a junior

Know the two options exist: transfer moves a buffer to one owner, shared memory lets several agents see the same bytes at once.

for a middle

Explain that transfer is already zero-copy, so shared memory buys concurrent access rather than speed, and that SharedArrayBuffer needs cross-origin isolation to exist.

for a senior

Show the working design — a pooled ping-pong protocol over transfers, measured first — and name the concrete symptoms that would force you to shared memory instead of assuming it up front.

for a principal

Own the whole-product tradeoff: a document-wide isolation policy and its effect on third-party embeds and popup flows, a timing-dependent bug class the team must be able to debug, and a fallback path for routes that lose isolation.

## Frame the choice as an ownership question Both mechanisms avoid copying. The difference is not performance, it is **how many agents may touch the bytes at once**. Transfer is a *move*: after `postMessage(buf, [buf])`, the sender's buffer is detached and only the receiver can read or write it. Exclusivity is enforced by the platform, not by convention. `SharedArrayBuffer` is *shared*: every agent that holds it sees the same memory simultaneously. Nothing stops two workers writing the same slot in the same instant, so correctness now depends on you coordinating access with `Atomics`. So the decision procedure is: describe the access pattern honestly. "One frame, one worker, then the result comes back" is a handoff, and transfer expresses it exactly. "Eight workers scan the same 200 MB asset while a producer appends to it" is concurrent access, and no amount of message passing makes that cheap. ## The case for transfer as the default Transfer costs nothing to deploy. It works on any page, over any origin, with any third-party scripts embedded, today. It is O(1) in buffer size. And the detachment semantics turn a whole class of concurrency bugs into a hard failure: if you write through a stale view after handing the buffer off, the write is silently dropped or the next view construction throws — bad, but local and findable, unlike a torn read. The pattern that makes transfer practical for a steady stream is a **buffer pool with ping-pong ownership**. Allocate N buffers up front, transfer one into a worker with the job, and have the worker transfer the *same* buffer back with its result. No allocation churn, no copying, and the pool size caps memory. With `navigator.hardwareConcurrency` workers and a couple of buffers in flight per worker, this saturates a pool without ever sharing a byte. What transfer cannot express: two agents needing the data at the same instant, or a producer that keeps appending while consumers read. Splitting the data so each worker owns a disjoint slice often rescues the design — if frames are independent, they are separate buffers, and the shared-memory case evaporates. ## The bill for SharedArrayBuffer Three costs, in the order that hurts. **Deployment.** `SharedArrayBuffer` exists only in a cross-origin isolated document: `Cross-Origin-Opener-Policy: same-origin` plus `Cross-Origin-Embedder-Policy: require-corp` on every HTML route, verified at runtime as `self.crossOriginIsolated`. That forces every cross-origin subresource to opt in and severs popup opener links. On a product with ad tags, embedded video and an OAuth popup, this is a cross-team migration, not a header. **Correctness.** Once memory is shared you own the synchronization. `Atomics.load`/`store`/`compareExchange` for the coordination slots, `Atomics.wait`/`notify` between workers — and remember the page's main thread is not allowed to block, so the waiting must live in workers. These bugs are timing-dependent: they pass in review, pass in CI, and appear on one user's eight-core laptop under load. **Maintenance.** A shared arena has no type checker. The layout — which offsets mean what, who may write which region — lives in comments and discipline. Every future contributor can violate it from anywhere. ## When shared memory is the right answer It earns its keep when the access pattern is inherently concurrent and the data is large enough that partitioning is not viable: a threaded WebAssembly runtime that expects a shared linear memory, an audio pipeline where a real-time thread and a UI thread meet at a lock-free ring buffer, a large read-only asset several workers index simultaneously. Note that the read-only case is the mildest — sharing an immutable buffer needs no atomics at all beyond publication, and is much easier to defend than a mutable arena. ## How I would actually run the decision Build the transfer version first, with a pooled ping-pong protocol, and measure. If the numbers meet the target, stop — you have avoided a document-wide policy change and an entire bug class. If they do not, identify precisely *why*: copying you failed to eliminate, message frequency, or genuine concurrent access. Only the third justifies shared memory. If you do adopt it, confine the shared region behind a small module with a documented layout, keep every blocking wait inside workers, and gate the whole path on `self.crossOriginIsolated` with a transfer-based fallback, because a route can lose isolation for reasons your team does not control.

  • What does a ping-pong buffer pool look like in practice?
    Allocate a fixed set of ArrayBuffers, transfer one into a worker along with the job, and have the worker transfer that same buffer back inside its reply. Ownership alternates, nothing is copied, allocation churn disappears, and pool size caps memory. The one rule is that neither side touches a buffer while it is away.
  • Is there a case where shared memory is easy to defend?
    Yes — a large read-only asset that many workers index at once. If nothing writes after publication, there are no races to coordinate and Atomics is barely involved. The remaining cost is the deployment one: the document still has to be cross-origin isolated for SharedArrayBuffer to exist at all.
  • How would you keep the design resilient if a route loses cross-origin isolation?
    Gate the shared-memory path on `self.crossOriginIsolated` and keep a transfer-based implementation behind the same interface. Isolation can be lost by a partner dropping a CORP header or a route served by another team, so treating it as a runtime capability rather than a build-time assumption avoids a hard failure in production.
  • Why does transfer eliminate a class of bugs rather than just a copy?
    Because detachment is enforced by the platform. After a transfer the sender physically cannot read or write those bytes, so two agents mutating the same memory is not a discipline question — it is impossible. Shared memory replaces that guarantee with a convention you have to uphold in every future change.

saying these in an interview costs you the question

  • Picking SharedArrayBuffer because it sounds faster than transfer
  • Ignoring the cross-origin isolation deployment cost
  • Assuming shared reads and writes are safe without Atomics
  • Believing transfer copies the buffer
  • Planning for the page's main thread to block on shared state

context