Performance and Clustering
One event loop is fast, but it is still one CPU core. Here is how to use all of them.
- explain the event-loop bottleneck and tell CPU-bound from I/O-bound work;
- use the
clustermodule to fork one worker per core and share a listening port; - describe how incoming connections are distributed (round-robin vs the OS);
- choose between
cluster,worker_threads, and horizontal scaling; - run production processes under a PM2-style manager with zero-downtime reloads;
- reason about load balancing, sticky sessions, and shared state across workers.
1. The event-loop bottleneck
You already know the event loop keeps a single thread busy by never blocking on I/O: while a database query or a file read is in flight, the loop services other callbacks. The catch is subtle but critical — your JavaScript itself still runs on one thread. Any callback that spends real CPU time (parsing a huge JSON payload, hashing a password with a large cost factor, resizing an image in pure JS, running a tight numeric loop) holds the thread hostage. During that time no other request can be served, no timer fires, and no I/O callback runs.
This is the difference that governs every scaling decision in Node:
- I/O-bound work (waiting on disk, network, database) — the loop stays free while the OS does the waiting. One process handles thousands of concurrent connections happily.
- CPU-bound work (computation that keeps the thread busy) — the loop is blocked. A single slow handler stalls every other client, and adding more concurrent requests only makes latency worse.
Analogy. A Node process is a single, extremely fast waiter. As long as guests are waiting on the kitchen (I/O), the waiter zips between many tables taking orders. But the moment you ask that one waiter to personally cook a 20-minute dish (CPU work), every other table waits, no matter how fast the waiter is. You do not fix this by asking the waiter to move faster — you hire more waiters. That is clustering.
A CPU-bound handler does not just slow itself down; it inflates the event-loop delay for everyone. Measuring that delay (for example with perf_hooks.monitorEventLoopDelay()) is the standard way to detect a blocked loop in production.