Node.js Worker Threads Data Sharing — Complete Guide to SharedArrayBuffer and Atomics
In this tutorial, you will learn about Node.js Worker Threads Data Sharing. We cover key concepts, practical examples, and best practices to help you master this topic.
Node.js worker threads data sharing uses SharedArrayBuffer for zero-copy memory access, Atomics for thread-safe operations, and message passing for structured data communication.
What You'll Learn
By the end of this tutorial, you'll share memory between workers using SharedArrayBuffer, use Atomics for safe concurrent access, transfer ownership with transferable objects, and choose the right sharing pattern.
Why Data Sharing Matters
Message passing copies data between threads. For large datasets, copying causes significant overhead. SharedArrayBuffer eliminates copies but requires synchronization.
Real-World Use
A real-time analytics pipeline shares a circular buffer of recent events via SharedArrayBuffer. Multiple workers write new events while the main thread reads for display, using Atomics for coordination.
Data Sharing Path
flowchart LR
A[Worker Threads] --> B[Data Sharing]
B --> C[N-API Addons]
C --> D[Performance]
D --> E[Memory Leaks]
B --> F{You Are Here}
style F fill:#f90,color:#fff
SharedArrayBuffer Basics
SharedArrayBuffer is an ArrayBuffer accessible from multiple threads simultaneously.
const { Worker } = require("node:worker_threads");
const sharedBuffer = new SharedArrayBuffer(256);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 42;
const worker = new Worker(`
const { parentPort } = require("worker_threads");
parentPort.on("message", (buf) => {
const arr = new Int32Array(buf);
arr[1] = arr[0] * 2;
parentPort.postMessage("done");
});
`, { eval: true });
worker.postMessage(sharedBuffer);
worker.on("message", () => {
console.log("Shared array:", sharedArray[0], sharedArray[1]);
});
Atomics for Synchronization
Atomics provides atomic operations on SharedArrayBuffer to prevent race conditions.
const { Worker } = require("node:worker_threads");
const buffer = new SharedArrayBuffer(4);
const counter = new Int32Array(buffer);
Atomics.store(counter, 0, 0);
const worker = new Worker(`
const { parentPort } = require("worker_threads");
parentPort.on("message", (buf) => {
const arr = new Int32Array(buf);
for (let i = 0; i < 100000; i++) {
Atomics.add(arr, 0, 1);
}
parentPort.postMessage("done");
});
`, { eval: true });
worker.postMessage(buffer);
for (let i = 0; i < 100000; i++) {
Atomics.add(counter, 0, 1);
}
worker.on("message", () => {
console.log("Final counter:", Atomics.load(counter, 0));
});
Message Passing for Complex Data
For complex data structures, use structured clone algorithm via postMessage.
const { Worker } = require("node:worker_threads");
const worker = new Worker(`
const { parentPort } = require("worker_threads");
parentPort.on("message", (data) => {
const processed = data.map((item) => ({
...item,
processed: true,
timestamp: Date.now(),
}));
parentPort.postMessage(processed);
});
`, { eval: true });
worker.postMessage([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
worker.on("message", (result) => {
console.log("Processed:", result.length, "items");
});
Transferable Objects
Transfer ArrayBuffer ownership to avoid copying large buffers.
const { Worker, isMainThread, parentPort } = require("node:worker_threads");
function createWorker() {
return new Worker(`
const { parentPort } = require("worker_threads");
parentPort.on("message", (buf) => {
const view = new Uint8Array(buf);
for (let i = 0; i < view.length; i++) view[i] = i % 256;
parentPort.postMessage("done");
});
`, { eval: true });
}
const buffer = new ArrayBuffer(1024 * 1024 * 10);
const worker = createWorker();
worker.postMessage(buffer, [buffer]);
console.log("Buffer detached:", buffer.byteLength === 0);
Worker Communication Patterns
Choose the right pattern based on data size and access frequency.
const patterns = {
sharedBuffer: "Use for large datasets, frequent reads/writes, zero-copy needed",
transferable: "Use for one-time transfer of large buffers, ownership transfer",
structuredClone: "Use for complex objects, infrequent communication, small data",
};
Object.entries(patterns).forEach(([name, desc]) => {
console.log(`${name}: ${desc}`);
});
Common Mistakes
1. Race Conditions Without Atomics
Multiple threads writing to the same SharedArrayBuffer without Atomics causes data corruption.
2. Using the Buffer After Transfer
Transferred buffers become detached. Accessing them after transfer throws errors.
3. Overusing SharedArrayBuffer for Small Data
For small messages, structured clone overhead is negligible. Use message passing for simplicity.
4. Forgetting SharedArrayBuffer Requires Specific Headers
HTTP responses must include Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers.
5. Not Using Atomics.wait for Synchronization
SharedArrayBuffer writes may not be visible to other threads without Atomics or synchronization.
Practice Questions
1. What is the advantage of SharedArrayBuffer over message passing?
Zero-copy access. Both threads read/write the same memory without Serialization.
2. What is Atomics.add used for?
Atomically adds a value to a SharedArrayBuffer element, preventing race conditions.
3. What happens to a transferred ArrayBuffer?
The sender's buffer becomes detached (length 0). Ownership moves to the receiver.
4. What headers are required for SharedArrayBuffer?
Cross-Opener-Policy: same-origin and Cross-Embedder-Policy: require-corp.
5. Challenge: Implement a shared counter using Atomics across multiple workers.
const buffer = new SharedArrayBuffer(4);
const counter = new Int32Array(buffer);
Atomics.store(counter, 0, 0);
const workers = Array.from({ length: 4 }, () => new Worker("./counter-worker.js"));
workers.forEach((w) => w.postMessage(buffer));
Promise.all(workers.map((w) => new Promise((r) => w.on("exit", r)))).then(() => {
console.log("Final count:", Atomics.load(counter, 0));
});
FAQ
Mini Project: Shared Counter with Multiple Workers
Build a parallel counter that aggregates results via shared memory.
const { Worker } = require("node:worker_threads");
const buffer = new SharedArrayBuffer(8);
const data = new Int32Array(buffer);
Atomics.store(data, 0, 0);
Atomics.store(data, 1, 0);
const code = `
const { parentPort } = require("worker_threads");
parentPort.on("message", (buf) => {
const arr = new Int32Array(buf);
for (let i = 0; i < 500000; i++) {
Atomics.add(arr, 0, 1);
Atomics.add(arr, 1, i);
}
parentPort.postMessage("done");
});
`;
const workers = Array.from({ length: 4 }, () => new Worker(code, { eval: true }));
workers.forEach((w) => w.postMessage(buffer));
Promise.all(workers.map((w) => new Promise((r) => w.on("exit", r)))).then(() => {
console.log("Count:", Atomics.load(data, 0));
console.log("Sum:", Atomics.load(data, 1));
});
What's Next
Node.js Worker Threads Node.js N-API Addons Node.js Performance
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro