Clustering & Worker Threads

15 questions found

A single process runs JavaScript on one main thread, using only one CPU core -- the cluster module forks multiple worker processes (typically one per core), each an independent copy of the app sharing the same listening port, letting a Node.js app utilize all available cores.
const cluster = require('node:cluster');
const os = require('node:os');
if (cluster.isPrimary) {
  for (let i = 0; i < os.cpus().length; i++) cluster.fork();
} else {
  require('./server');
}
Real-world example An API deployed to an 8-core server using only one core adds cluster.fork() to spawn eight workers, roughly increasing throughput eight-fold under CPU-bound load.

Common follow-ups: How does the cluster module's shared-port mechanism distribute connections across workers?;What state can be shared between cluster workers, given they're separate OS processes?

Advanced Node.js;Performance Optimization & Profiling
Each cluster worker is a separate OS process with its own memory -- an in-memory cache exists separately and inconsistently in each worker. The fix is moving shared state to an external store like Redis, accessible consistently regardless of which worker handles a given request.
// Broken with clustering: each worker has its own separate cache
const cache = new Map();

// Correct: external shared store
await redis.set(key, value);
Real-world example A team clustering across four cores noticed roughly 1 in 4 requests seemed to miss recently cached data, traced to an in-memory Map existing separately in each worker; moving to Redis resolved it.

Common follow-ups: What's the correct way to implement rate limiting correctly across cluster workers given this same issue?;How does this apply to WebSocket connections handled by a specific worker?

Caching with Redis;WebSockets & Real-Time Communication
PM2's cluster mode achieves the same goal at the process-manager level, without needing cluster-aware application code, and additionally provides zero-downtime reloads, automatic restarts, and log aggregation that would need to be built manually with the raw cluster module.
pm2 start server.js -i max --name my-app

// server.js needs zero cluster-specific code
const app = express();
app.listen(3000);
Real-world example A team migrates from a hand-rolled cluster.js entry point to 'pm2 start server.js -i max', immediately gaining zero-downtime reloads without maintaining that logic themselves.

Common follow-ups: What does 'zero-downtime reload' mean operationally, and how does PM2 achieve it?;When would you still want the raw cluster module instead of PM2?

Deployment & Process Managers (PM2);Advanced Node.js
SharedArrayBuffer allocates memory multiple worker threads can access directly without copying -- writes from one thread are immediately visible to others, requiring careful synchronization via Atomics to avoid race conditions from concurrent unsynchronized writes.
const sab = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sab);
Atomics.add(sharedArray, 0, 1);
console.log(Atomics.load(sharedArray, 0));
Real-world example A parallel computation service divides a matrix operation across four worker threads reading/writing different slices of the same SharedArrayBuffer, avoiding the overhead of copying data via message passing.

Common follow-ups: Why must operations on shared memory use Atomics rather than regular read/write?;What are the practical limits on data types inside a SharedArrayBuffer?

Advanced Node.js;Performance Optimization & Profiling
postMessage() sends data using the structured clone algorithm, supporting most types (objects, arrays, Maps, typed arrays) but not functions or DOM nodes -- transferable objects like ArrayBuffers can be moved rather than copied for zero-copy performance, at the cost of the sender losing access afterward.
worker.postMessage({ command: 'process', data: [1, 2, 3] });

const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage({ buffer }, [buffer]); // transferred, zero-copy
Real-world example An image-processing worker receives a large pixel buffer via a transferable ArrayBuffer rather than a structured-clone copy, avoiding duplicating tens of megabytes on every message.

Common follow-ups: What happens if you try to postMessage() a function or class instance with methods?;What's the performance difference between structured-clone copying and transferring an ArrayBuffer?

Advanced Node.js;File Uploads & Media Processing
Creating a new Worker has meaningful startup overhead -- a worker pool pre-creates a fixed number of threads once at startup and reuses them across many tasks, queuing work when all are busy, amortizing thread-creation cost across many tasks.
const pool = new Piscina({ filename: './cpu-task.js' });
app.post('/process', async (req, res) => {
  const result = await pool.run(req.body);
  res.json(result);
});
Real-world example An endpoint performing CPU-intensive PDF generation uses a Piscina pool of four pre-warmed workers rather than spawning a new Worker per request, eliminating a per-request latency spike.

Common follow-ups: How do you size a worker pool relative to available CPU cores?;What happens to queued tasks if every worker is busy when a new request arrives?

Performance Optimization & Profiling;Advanced Node.js
By default the primary distributes connections across workers round-robin-like. 'Sticky' load balancing ensures requests from the same client always route to the same worker -- important for WebSocket connections or scenarios relying on in-memory, per-connection state on a specific worker.
const sticky = require('sticky-session');
if (!sticky.listen(server, 3000)) {
  server.once('listening', () => console.log('Server started'));
}
Real-world example A chat app clustering across four cores discovers WebSocket reconnections sometimes landed on a different worker than the one holding session state; sticky load balancing by IP resolved dropped connections.

Common follow-ups: Why is sticky load balancing generally unnecessary if session state is already externalized to Redis?;How does sticky behavior interact with a cloud load balancer in front of the whole cluster?

WebSockets & Real-Time Communication;HTTP & HTTPS Modules
Zero-downtime restart replaces workers one at a time -- each old worker finishes in-flight requests and stops accepting new ones while a new worker starts, so at every point some workers remain available, meaning no dropped requests during a deployment.
pm2 reload my-app
// PM2 starts a new worker, waits for it to be ready, then gracefully
// shuts down one old worker at a time until all run the new code
Real-world example An e-commerce API deploys several times daily using 'pm2 reload' rather than 'pm2 restart', so customers mid-checkout never experience a dropped connection during routine deployments.

Common follow-ups: What's the difference between 'pm2 restart' and 'pm2 reload' regarding downtime?;How long should a worker be given to drain requests before forced termination during a reload?

Deployment & Process Managers (PM2);Docker & Containerization for Node.js
A common rule of thumb is one worker per CPU core, but the optimal number depends on workload -- a CPU-bound app benefits most from exactly matching cores, while a heavily I/O-bound app might benefit from slightly more workers, since extra workers can progress on other requests while some are blocked on I/O.
const numWorkers = process.env.WORKER_COUNT || os.cpus().length;
for (let i = 0; i < numWorkers; i++) cluster.fork();
Real-world example A CPU-intensive transcoding service settles on exactly one worker per core after benchmarks showed more workers slowed things down, while a mostly I/O-bound API performs best with roughly 1.5x the core count.

Common follow-ups: How would you actually benchmark the optimal worker count for a real workload?;Does this intuition apply equally to worker threads used for CPU-bound tasks?

Performance Optimization & Profiling;Advanced Node.js
Because each worker is an independent process, a crash doesn't directly crash the others -- but the crashed worker stops serving traffic until replaced. The primary should listen for 'exit' and fork a replacement, restoring capacity quickly while the error is still logged for investigation.
cluster.on('exit', (worker, code, signal) => {
  console.error(`Worker ${worker.process.pid} died. Forking a replacement.`);
  cluster.fork();
});
Real-world example A production cluster automatically forks a replacement worker within milliseconds of any crash, keeping total capacity roughly constant while alerting the on-call engineer to investigate.

Common follow-ups: What's the risk of a worker crashing and restarting in a tight loop if a bug is triggered by every request of a certain type?;How would you add backoff or a circuit-breaker to the fork-on-exit logic?

Error Handling;Deployment & Process Managers (PM2)

Showing 1–10 of 15