Module A-7·27 min read

cluster vs worker_threads, SharedArrayBuffer ring buffers for zero-copy IPC, and parallelising transaction signature verification.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module 6 — Core Scaling: Multi-Process Clustering & IPC Latency

What this module covers: A single Node.js process is single-threaded. On a 32-core cloud instance, that means 31 cores sit idle while your blockchain indexer saturates the 32nd. cluster and worker_threads are the two mechanisms for using those cores — but they have fundamentally different properties, different communication overhead, and different failure modes. Choosing the wrong one for your workload costs 3–10x throughput. This module covers the exact internal mechanics of both, the precise cost of IPC serialization at high message rates, and how SharedArrayBuffer + Atomics eliminates that cost for the right workloads.


Why a Single Node.js Process Cannot Saturate a Multi-Core Server

The event loop runs on one thread. JavaScript runs on one thread. V8 runs on one thread. A 32-core server with Node.js running on it has 31 cores available for your application — and by default, 31 of them are idle.

This is by design, not a limitation. The single-threaded model is what makes the event loop's performance guarantees possible: no shared state, no mutex contention, no thread-safety bugs. But for CPU-intensive workloads on large instances, you need to take deliberate action to use the hardware you're paying for.

Two mechanisms exist:

  • cluster: forks full Node.js processes. Each has its own V8 heap, event loop, and thread. Complete process isolation.
  • worker_threads: creates threads within a single process. Shared memory space. Communication via message passing or SharedArrayBuffer.

The right choice depends entirely on your workload characteristics.


cluster: Full Process Replication

cluster.fork() creates a complete copy of the Node.js process. The child process has its own V8 heap, its own event loop, its own module registry, and its own memory space.

javascript

Cluster Load Balancing: Round-Robin vs SO_REUSEPORT

Default (round-robin by master): The master process accepts all incoming connections and distributes them to workers via IPC. This creates a bottleneck at the master process and adds IPC overhead per connection.

SO_REUSEPORT (recommended): Each worker binds directly to the port. The kernel distributes connections between workers using a hash of the connection 4-tuple (src IP + src port + dst IP + dst port). No IPC for connection distribution. No master bottleneck.

javascript

Why SO_REUSEPORT is better for blockchain indexers:

The master-round-robin model concentrates the accept syscall on one process. At 50K connections/second, the master spends all its time calling accept4 and sending socket handles to workers via IPC. With SO_REUSEPORT, 32 workers each accept 1,562 connections/second — well within the capacity of each worker's event loop.

The Memory Cost of cluster

Each forked worker is a separate process. On Linux, fork() uses copy-on-write, so immediately after forking, the child shares the parent's pages. As each worker modifies pages (loading modules, creating objects), those pages are copied — the shared advantage erodes over time.

For a typical Node.js application with a 200MB heap, 32 workers will consume:

  • Immediately after fork: ~200MB shared + ~50MB unique per worker = ~1.8GB
  • After warmup (1–2 min): ~200MB shared + ~150MB unique per worker = ~5GB

On a 32GB server, 32 workers at 5GB total leaves 27GB for OS page cache and application data — acceptable. On a 16GB server with a large application, cluster may not be viable.

Graceful Rolling Restarts: worker.disconnect() for Zero-Downtime Deploys

For a payments gateway, killing workers outright on every deploy means every in-flight settlement request gets dropped mid-transaction. cluster gives you worker.disconnect() specifically to avoid that: it closes the worker's IPC channel and, for servers created with net.createServer/http.createServer, stops the worker from accepting new connections while letting existing connections finish naturally.

javascript

The key property that makes this safe for a payments narrative: disconnect() does not terminate in-flight requests. A worker mid-way through validating a UPI transfer keeps running until that request completes (or the safety-net timeout forces it), while the newly forked replacement is already accepting traffic. Compare this to worker.kill() or a bare process restart, which drops in-flight work immediately — unacceptable for a payment gateway mid-settlement.


child_process: Spawning External Processes

For integrating with non-JavaScript components (a Rust binary for signature verification, a Python analytics script, a Go RPC service), child_process provides three key APIs.

spawn: Streaming I/O

javascript

spawn is for processes that produce large output or need streaming I/O. stdout/stderr are Readable streams — backpressure applies.

fork: V8-to-V8 IPC

fork is spawn specialized for Node.js child processes. It creates a communication channel (IPC) that supports process.send() / process.on('message').

javascript

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.