AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 17 — Production and Threads — PM2, Cluster, Child Processes, Workers

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

August 13, 20267 min read
Download as Markdown

"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!