17 — Production and Threads — PM2, Cluster, Child Processes, Workers

7 min read

"Just run the server and hope" was my production strategy, and hope stopped being enough at the first crash. The model that finally stuck splits into two halves: PM2 keeps a Node process alive forever across crashes and reloads, and Node offers three distinct concurrency primitives — child_process, cluster, and worker_threads — each for a different job. [1] Knowing which primitive solves which problem is what turns "Node is single-threaded" from a limitation into a design choice.

The framing that landed for me is "keep it alive" first, then "do more than one thread can."

PM2: the production process manager

In development, I run node server.js and it stays alive until I kill it or it crashes. In production, that is not good enough — a crash should not take the site down, deployments should not drop in-flight requests, and I want visibility into resource usage. PM2 is a production process manager that wraps the Node process and provides exactly those guarantees [2][3].

pm2 start server.js --name "api" -i max
pm2 reload "api" # zero-downtime reload
pm2 logs # aggregated logs
pm2 monit # CPU/memory dashboard

PM2's core promises:

  • Keep alive — if the process crashes, PM2 restarts it automatically.
  • Zero-downtime reload — pm2 reload starts new workers and only then stops the old ones, so in-flight requests finish.
  • Cluster mode — with -i max, PM2 runs one worker per CPU core (more on this below).
  • Logs and monitoring — aggregated stdout/stderr across all workers, plus a live CPU/memory view.

The key distinction from the dev watcher (Nodemon, --watch): a dev watcher restarts on file change; PM2 restarts on crash and keeps the process alive in production. They are different tools for different phases.

Escaping the single thread: three primitives

Node.js runs JavaScript on a single main thread — that is the design that makes non-blocking I/O cheap. But there are real workloads where one thread is not enough: a multi-core CPU sitting mostly idle, a CPU-heavy computation that would block the event loop, or a need to run a separate program. Node offers three primitives for these cases, and they are not interchangeable [1].

child_process main child separate OS process no shared memory · messages run a program, shell out cluster primary worker worker worker N workers share one port worker_threads main thread worker worker in-process · shared memory offload CPU work

child_process: run another program

The child_process module lets Node spawn separate OS processes — run a shell command, execute another script, pipe data to and from a child [4]. The three main methods are spawn (streaming, for long-running processes), exec (buffered, for a one-shot command), and fork (a Node-specific spawn with an IPC channel — a way for the parent and child to send messages back and forth — built in). Each child is a full separate process with its own memory.

import { exec } from 'child_process';

exec('git rev-parse HEAD', (err, stdout) => {
console.log('commit:', stdout.trim());
});

The use case is "run something that is not Node" — a shell command, a build tool, a script in another language. The caution is security: passing user input to exec is a command-injection vector, so user input must be sanitized or passed via spawn with an argument array (which does not invoke a shell).

cluster: scale across CPU cores

The cluster module lets a primary process fork multiple identical worker processes, all sharing the same port [5]. The primary accepts connections and distributes them round-robin to the workers. Because each worker is a separate process with its own event loop, the application handles N times the load on an N-core machine.

import cluster from 'cluster';
import os from 'os';

if (cluster.isPrimary) {
for (let i = 0; i < os.cpus().length; i++) cluster.fork();
} else {
// each worker runs the server
app.listen(3000);
}

The use case is horizontal scaling on a single machine — turning one underutilized core into N utilized cores. This is exactly what PM2's cluster mode (-i max) automates, so in practice I rarely write cluster code by hand; PM2 wraps it.

worker_threads: CPU-bound work without blocking the loop

worker_threads are the right answer for CPU-heavy JavaScript that would block the main event loop [6]. Unlike child processes, worker threads run within the same process and can share memory (via SharedArrayBuffer and ArrayBuffer transfers), which makes them cheaper than spawning a process.

import { Worker } from 'worker_threads';

const worker = new Worker('./heavy.js');
worker.postMessage({ input: largeArray });
worker.on('message', result => console.log('done:', result));

The use case is "do heavy computation in JavaScript without freezing the main thread" — image processing, hashing, large sorts, compression. The main thread stays responsive to I/O; the worker does the math. For I/O-heavy work, worker threads are unnecessary (the event loop already handles that); they exist specifically for CPU-bound work.

How I use this

The model I keep is "PM2 for alive, then the three primitives for the three problems." In production I run under PM2 with cluster mode (-i max), which gives me crash recovery, zero-downtime reloads, and one worker per core — that combination covers most production needs without me writing cluster code. I reach for child_process when I need to run an external program or shell command, passing user input safely via spawn's argument array rather than exec's shell string. I reach for worker_threads only for genuine CPU-bound work — image transforms, crypto, large data processing — where the alternative would freeze the event loop. The framing — "alive" is a process-manager job, "more than one thread" depends on whether the bottleneck is I/O (none needed), CPU (worker_threads), or total throughput on multi-core (cluster) — is what keeps the choices straight.

References

[1] Alvin Lal, "Single Thread vs Child Process vs Worker Threads vs Cluster in Node.js." [Online]. Available: https://alvinlal.netlify.app/blog/single-thread-vs-child-process-vs-worker-threads-vs-cluster-in-nodejs

[2] "PM2," pm2.keymetrics.io. [Online]. Available: https://pm2.keymetrics.io/

[3] Better Stack, "Running Node.js Apps with PM2 (Complete Guide)." [Online]. Available: https://betterstack.com/community/guides/scaling-nodejs/pm2-guide/

[4] OpenJS Foundation, "Child process," Node.js API Docs. [Online]. Available: https://nodejs.org/api/child_process.html#child-process

[5] OpenJS Foundation, "Cluster," Node.js API Docs. [Online]. Available: https://nodejs.org/api/cluster.html#cluster

[6] OpenJS Foundation, "Worker threads," Node.js API Docs. [Online]. Available: https://nodejs.org/api/worker_threads.html#worker-threads

Knowledge check · Question 1 of 5

What does PM2 provide that `node server.js` alone does not?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!