worker_threads give you real parallelism for CPU-bound work. SharedArrayBuffer gives you a way to corrupt state at nanosecond speed if you invent a protocol casually. The senior pattern is boring: one writer owns each region, Atomics coordinate handoff, and ordinary JS objects never cross the boundary except as structured-clone messages for control plane events.
⚡ TL;DR: Use SAB for numeric payloads and ring buffers only; gate every multi-byte write with
Atomics(or transfer ownership so only one thread touches a slice); keep request metadata onMessagePort; fuzz your protocol undernode --testwith thread sanitizers in CI where available. Pair with AI Test Generation: Property Tests That Catch Race Conditions for concurrency oracles.
Ownership beats locking theater
If two threads can write the same index without a happens-before edge, you do not have a protocol — you have a race. Prefer single-producer/single-consumer rings.
// src/sab-ring.ts
const HEAD = 0; // Int32 index in control SAB
const TAIL = 1;
export function createRing(slots: number, slotBytes: number) {
const control = new SharedArrayBuffer(8); // 2 * int32
const data = new SharedArrayBuffer(slots * slotBytes);
const ctrl = new Int32Array(control);
Atomics.store(ctrl, HEAD, 0);
Atomics.store(ctrl, TAIL, 0);
return { control, data, slots, slotBytes };
}
export function tryPush(ctrl: Int32Array, data: Uint8Array, slots: number, slotBytes: number, payload: Uint8Array) {
const head = Atomics.load(ctrl, HEAD);
const tail = Atomics.load(ctrl, TAIL);
if ((head + 1) % slots === tail) return false; // full
data.set(payload, head * slotBytes);
// ✅ Publish payload before advancing head
Atomics.store(ctrl, HEAD, (head + 1) % slots);
Atomics.notify(ctrl, HEAD, 1);
return true;
}
export function tryPop(ctrl: Int32Array, data: Uint8Array, slots: number, slotBytes: number, out: Uint8Array) {
const head = Atomics.load(ctrl, HEAD);
const tail = Atomics.load(ctrl, TAIL);
if (head === tail) return false;
out.set(data.subarray(tail * slotBytes, tail * slotBytes + out.length));
Atomics.store(ctrl, TAIL, (tail + 1) % slots);
return true;
}
// ❌ Shared mutable object via workerData — not shared, not safe if you "fix" it with SAB tricks
workerData: { state: { counters: {} } }
Control plane on MessagePort, data plane on SAB
Config updates, cancel tokens, and error objects belong on the message channel. Bytes and floats belong on SAB.
// src/cpu-pool.ts
import { Worker } from "node:worker_threads";
import { createRing } from "./sab-ring.js";
export function startPool(n: number) {
const ring = createRing(1024, 64);
const workers = Array.from({ length: n }, () => {
const w = new Worker(new URL("./cpu-worker.js", import.meta.url), {
workerData: { control: ring.control, data: ring.data, slots: ring.slots, slotBytes: ring.slotBytes },
});
w.on("message", (msg) => {
if (msg.type === "fatal") metrics.increment("pool.worker_fatal");
});
return w;
});
return { ring, workers };
}
Atomics wait without wedging the event loop
Atomics.wait blocks the worker thread — that is fine on workers, catastrophic on the main thread. On the main thread use Atomics.waitAsync (or message notifications) so you do not recreate the stalls covered in Node.js Event Loop Lag: Catch P99 Stalls.
// cpu-worker.ts
import { workerData, parentPort } from "node:worker_threads";
const ctrl = new Int32Array(workerData.control);
const data = new Uint8Array(workerData.data);
while (true) {
// ✅ Block only inside the worker
while (!tryConsume()) {
Atomics.wait(ctrl, 0 /* HEAD */, Atomics.load(ctrl, 0));
}
}
function tryConsume() {
// pop + compute + optional MessagePort result for large JS values
return false;
}
Testing races on purpose
Deterministic seeds + forced interleaving beat “it passed on my laptop.”
import { test } from "node:test";
import assert from "node:assert/strict";
test("ring never duplicates slots under contention", async () => {
// spawn 1 producer + 4 consumers, push N items, assert set size === N
assert.equal(1, 1); // replace with real harness
});
Closing checklist
✅ Dos
– ✅ Single-writer regions with Atomics publish order
– ✅ MessagePort for objects/errors/cancel
– ✅ Atomics.wait only on worker threads
– ✅ Fuzz fill/empty boundaries and wraparound
– ✅ Document endianness and slot layout in one ADR
❌ Don’ts
– ❌ Don’t share ordinary JS objects across threads
– ❌ Don’t use non-atomic multi-byte reads/writes on SAB
– ❌ Don’t Atomics.wait on the main thread
– ❌ Don’t grow SAB protocols without a version field
– ❌ Don’t ignore worker error / exit events
Related reading
- AI Test Generation: Property Tests That Catch Race Conditions
- Node.js Event Loop Lag: Catch P99 Stalls
- AST-Guided Edits: LLMs Propose Intents and Codemods Apply Them
- OpenTelemetry for LLMs: Trace Prompt Latency Across Microservices
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
