Say you've got worker threads, child processes, and cluster all on the table. How do you figure out which one a given problem actually needs?
The strong answers pick the primitive by working out what needs shared memory and what needs to fail alone, instead of reciting the API list.
What an AI-prepared candidate might say
So they're for different things, basically. worker_threads gives you actual threads inside the same process, which is what you want for CPU-heavy stuff like parsing or image processing or crypto, and they can share memory through SharedArrayBuffer, plus they're cheaper than processes. child_process spawns a whole separate OS process. You'd use that to run other executables, or when you want real isolation, and it talks back to the parent over stdio or an IPC channel. And cluster forks a bunch of Node processes that all share one server port, so your HTTP server can use every core on the machine. The rule of thumb I know is cluster or your orchestrator for scaling requests, worker threads to get CPU work off the event loop, and child processes for external programs or anything you want isolated from the main app.
Senior
I ask three questions, in order. First, what's actually saturated? If the event loop is getting blocked by CPU-bound JavaScript, fine, parallelism will help. But if the service is I/O-bound, none of these primitives buys you any throughput, the loop already multiplexes I/O just fine and your fix is somewhere else entirely.
Second, what needs to be shared. Threads live in one process, so they can share ArrayBuffer memory zero-copy through SharedArrayBuffer, and they can transfer buffers between isolates without copying. Processes have to serialize everything over an IPC pipe, and that cost grows with the payload. So big binary inputs and outputs push me toward threads. If the tasks are small descriptors, processes are just as cheap, honestly.
Third, what needs to be isolated. A thrown error in a worker shows up as an 'error' event on the parent's handle and the process keeps going. But threads share the process. A native-addon crash or an OOM takes every thread down together. A child process is kernel-isolated, it can segfault or leak or get OOM-killed all on its own, and the parent just sees an exit.
One thing on weight, because people get this wrong constantly. Every worker thread is a full V8 isolate, its own heap, its own event loop. Cheaper and faster to spawn than a process, sure, but nothing like a goroutine. That's why pools exist. Cluster's the odd one out here, it's child_process.fork plus listening-socket coordination so N processes can serve one port, which gets you request-throughput scaling with process isolation and no shared memory. And if you need to run an external binary, there's exactly one door, and it's child_process.
Staff
At this level it stops being about the APIs and turns into sizing and deployment context. Threads and processes pull from the same core budget, and people forget that constantly. A cluster of eight workers where each one runs a four-thread pool, that's thirty-two runnable threads on an eight-core box. The oversubscription doesn't show up as throughput, it shows up as context-switch overhead and p99 jitter. I set the budget once, globally, from os.availableParallelism(), and every pool divides it.
Memory limits are per-isolate, which bites people. --max-old-space-size governs each process, worker threads take resourceLimits, and a fleet of isolates each entitled to a big heap can promise the container more memory than it actually has. And the OOM kill lands at the process level no matter which isolate did the allocating.
The Kubernetes question deserves a straight answer. If the platform's already running N single-process pods behind a service, cluster inside each pod is mostly duplicating the platform's job. The honest cases left are bin-packing big nodes with fewer, larger pods, per-core licensing shapes, or squeezing shared-nothing throughput out of a fixed VM. Otherwise one process per pod keeps health checks, memory accounting, and restart semantics lined up with what the orchestrator thinks is happening.
What actually picks the primitive is arithmetic on the workload. Count the bytes moved per task, serialization cost favors threads as payloads grow. Weigh tasks per second against spawn cost, though pools amortize that either way. Ask how much shared fate you can stomach, native code and memory pressure push you toward processes. And be honest about operational appetite, every thread pool you add is queueing and saturation and observability you now own inside the process, where the platform can't see any of it.
Follow-up chain
- People say worker threads share memory. What actually gets shared?
- Okay, so if I postMessage a 100 MB object across, what does that cost me, and how do I avoid paying it?
- Say a native addon segfaults inside one of your worker threads. How much goes down with it?
- You're already on Kubernetes. Is cluster ever still worth running?