awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

SharedArrayBuffer, Atomics, and cross-origin isolation

Crux SAB is real shared memory between threads, gated behind COOP + COEP; Atomics prevents data races; the relaxed memory model means atomic operations are the only safe ordering guarantee.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 18 min

Every postMessage either copies or hands off ownership. But multithreaded WASM needs a shared ring buffer that both the codec threads and JS can read and write at the same time — no copy, no handoff, just shared memory. And then it silently returns undefined in production.

What SharedArrayBuffer is

SharedArrayBuffer (SAB) is a block of memory that two or more threads can both read and write at the same time, with no copy. You build typed-array views over it on each thread and they see the same bytes:

// main thread
const sab = new SharedArrayBuffer(1024);
worker.postMessage({ sab }); // NOT transferred — shared

// worker
self.onmessage = e => {
  const view = new Int32Array(e.data.sab);
  view[0] = 42; // visible to main thread immediately
};

This is the foundation that makes multithreaded WebAssembly work in the browser — WASM’s linear memory is a SharedArrayBuffer when compiled with -pthread.

Why Atomics exist

Two threads writing the same SAB location without coordination is a data race — the result depends on timing and is undefined. Atomics provides operations the CPU guarantees are indivisible:

  • Atomics.add(view, i, 1) — reads, adds, writes as one uninterruptible step. Two threads incrementing the same counter never lose an update.
  • Atomics.compareExchange(view, i, expected, replacement) — the building block for lock-free data structures.
  • Atomics.wait(view, i, expected)blocks the calling thread until another thread calls Atomics.notify(view, i). A real blocking primitive — forbidden on the main thread, allowed inside workers. This wait/notify pair is how a WASM thread pool parks idle workers and wakes them when work arrives.
SharedArrayBuffer at a glance
Requires
COOP: same-origin + COEP: require-corp
crossOriginIsolated check
self.crossOriginIsolated === true
Without headers
typeof SharedArrayBuffer === 'undefined'
Atomics.wait
Forbidden on main thread — blocks the thread
Growable SAB
new SharedArrayBuffer(size, { maxByteLength })

The relaxed memory model

SAB exposes a relaxed memory model: without atomics, one thread’s writes are not guaranteed visible to another thread in any particular order — the CPU and compiler may reorder non-atomic memory operations.

Atomic operations establish synchronisation edges (happens-before relationships) that make preceding non-atomic writes visible. The practical rule for application code:

  1. Worker writes bulk data with plain typed-array writes.
  2. Worker does one Atomics.store on a “data ready” flag.
  3. Reader does one Atomics.load on the flag.
  4. Once the reader sees the ready value, all the bulk writes are guaranteed visible.

Hand-rolling lock-free structures without this discipline produces bugs that appear only under load, only on certain CPUs.

The COOP/COEP gate

After the Spectre CPU vulnerability (2018), high-resolution shared memory became a security risk — a SAB-backed timer is precise enough to mount a side-channel attack. Browsers locked SAB behind two response headers the document must send:

  • Cross-Origin-Opener-Policy: same-origin (COOP) — isolates your browsing-context group from other origins.
  • Cross-Origin-Embedder-Policy: require-corp (COEP) — requires every cross-origin subresource to explicitly opt in to being embedded via Cross-Origin-Resource-Policy.

When both are present, self.crossOriginIsolated is true and SharedArrayBuffer is available. When they are not, SharedArrayBuffer is undefined and any code that needs it — multithreaded WASM especially — silently fails.

The cost: COEP breaks cross-origin images, fonts, and scripts that do not send the matching Cross-Origin-Resource-Policy header. Enabling isolation means auditing and fixing every third-party asset. Roll out COEP in report-only mode first (Cross-Origin-Embedder-Policy-Report-Only) to enumerate what would break before enforcing.

Debug this
log
> typeof SharedArrayBuffer
'undefined'

> self.crossOriginIsolated
false

> performance.getEntriesByType('navigation')[0].responseHeaders
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: (not set)

[WASM] threads requested: 8
[WASM] SharedArrayBuffer unavailable - falling back to single-threaded
[WASM] init complete (single-threaded, 4.2x slower than target)

A multithreaded WASM module silently runs single-threaded in production. The console shows SharedArrayBuffer is undefined and crossOriginIsolated is false. COOP is set but COEP is not. What is the exact fix, and what will it break?

Quiz

Why is `Atomics.wait()` forbidden on the main thread but allowed inside a worker?

Trace it
1/4

A page deploys COOP + COEP to unlock multithreaded WASM. After deploy, crossOriginIsolated is true but the WASM module panics on startup with 'memory.grow failed'. Debug the cause.

1
Step 1 of 4
The WASM module uses growable SharedArrayBuffer (maxByteLength option) but the browser does not support it
2
Locked
The WASM runtime called memory.grow without atomic coordination — another thread was mid-read of the old memory region
3
Locked
The SharedArrayBuffer size exceeds browser limits
4
Locked
COEP broke one of the WASM imports
Why this works

Why does the Spectre gate exist? Spectre is a CPU microarchitecture vulnerability (CVE-2017-5753) where speculative execution leaks memory across process boundaries via timing side-channels. High-resolution timers (including those you can build from a SharedArrayBuffer by incrementing a counter in one thread while measuring reads in another) make these attacks practical in the browser. The COOP header prevents your page from sharing a process with potentially malicious cross-origin pages; COEP prevents cross-origin resources from being loaded without explicit consent. Together they give the browser confidence it can enable high-resolution shared memory without leaking secrets from co-located origins.

Recall before you leave
  1. 01
    Walk through everything that has to be true for SharedArrayBuffer to be available, and what enabling it costs.
  2. 02
    Why are atomic operations necessary when using SharedArrayBuffer?
  3. 03
    What is the practical pattern for using an atomic flag to signal 'data is ready' in a SharedArrayBuffer?
Recap

SharedArrayBuffer is the escape from message-passing — a block of memory visible to all threads that hold a view of it, with zero copy overhead. It is the foundation of multithreaded WASM in the browser. It is gated behind COOP: same-origin and COEP: require-corp (or credentialless) — without both headers, SharedArrayBuffer is undefined and multithreaded WASM silently degrades. Atomics prevents data races: Atomics.add, Atomics.compareExchange, and Atomics.wait/notify are the primitives. Atomics.wait is forbidden on the main thread because it blocks — and blocking the main thread freezes the page. The relaxed memory model means only atomic operations guarantee ordering between threads.

Connected lessons
appears again in41
Continue the climb ↑Service worker edge cases: version skew, durability, and navigation traps
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.