Browser & Frontend Runtime
SharedArrayBuffer, Atomics, and cross-origin isolation
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 callsAtomics.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.
- 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:
- Worker writes bulk data with plain typed-array writes.
- Worker does one
Atomics.storeon a “data ready” flag. - Reader does one
Atomics.loadon the flag. - 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 viaCross-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.
> 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?
Why is `Atomics.wait()` forbidden on the main thread but allowed inside a worker?
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.
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.
- 01Walk through everything that has to be true for SharedArrayBuffer to be available, and what enabling it costs.
- 02Why are atomic operations necessary when using SharedArrayBuffer?
- 03What is the practical pattern for using an atomic flag to signal 'data is ready' in a SharedArrayBuffer?
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.
appears again in41
- Federation and lookahead: batching beyond DataLoadermiddle
- Senior GraphQL API: scheduling contract, tenant isolation, observabilitysenior
- Lock and single-flight: bounding concurrent rebuildsmiddle
- Stale-while-revalidate and CDN request coalescingmiddle
- Detecting stampedes and designing TTL for productionmiddle
- Metastable failure, fencing tokens, and production postmortemssenior
- What a relation is: tables, rows, keys, and constraintsjunior
- Constraints, keys, and Postgres data typesmiddle
- JSONB, arrays, and when a side table winsmiddle
- Schema integrity: deferral, versioning, and production failure modessenior
- Where data fetching happens — and why it decides LCPjunior
- React Server Components and Suspense streamingmiddle
- Senior internals: RSC payload, caching layers, and production failure modessenior
- The IP envelopejunior
- Reading the IP headermiddle
- What TLS does and why it existsjunior
- Key schedule, SNI, ALPN, and extensionssenior
- 0-RTT defenses, ECH, hybrid PQ, and production TLSsenior
- The twelve layers: one URL, seven actorsjunior
- Resilience: cascading retries, circuit breakers, and error budgetssenior
- What is OpenTelemetry: API, SDK, Collector, OTLPjunior
- OTel signals, Semantic Conventions, and the OTLP wire formatmiddle
- The OTel Collector: receivers, processors, exporters, and deployment patternsmiddle
- Vendor neutrality, eBPF instrumentation, the Operator, and browser/serverless OTelsenior
- Operating the OTel Collector: reliability, version skew, failure modes, and governancesenior
- What is trace propagation and why broken propagation is worse than nonejunior
- traceparent and tracestate: the W3C header format in fullmiddle
- Baggage and async boundaries: carrying context across queues and callbacksmiddle
- Async context per language, service mesh, B3 migration, and securitysenior
- Production propagation failures, span links, and platform designsenior
- The debugging funnel: SLO → RED → trace → profilejunior
- OTel architecture: one SDK, four signals, one wire formatmiddle
- The incident loop: from pager to postmortem to preventionmiddle
- Scale, security, and the ROI of observable systemssenior
- At-most-once, at-least-once, exactly-once: the three delivery contractsjunior
- Consumer-side dedup: the cheapest path to exactly-once processingmiddle
- Exactly-once in production: impossibility proof, hybrid patterns, and real incidentssenior
- What OAuth is and why passwords are not the answerjunior
- Authorization code flow with PKCEmiddle
- Sender-constrained tokens: DPoP and mTLSsenior
- OAuth in production: audience attacks, observability, and real failuressenior