---
title: "17 — Production and Threads — PM2, Cluster, Child Processes, Workers"
uid: production-and-threads
tags: ["production", "child-process", "nodejs", "cluster", "worker-threads", "concurrency", "roadmap:nodejs", "pm2"]
excerpt: "PM2 keeps the process alive across crashes; three concurrency primitives — child_process, cluster, worker_threads — each solve a different job. Knowing which is which turns the single thread from limitation into design choice."
date: 2026-08-13T03:27:54+0000
source: https://www.aveshina.my.id/en/blog/production-and-threads
---

"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].

```figure
<svg viewBox="0 0 740 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Three Node.js concurrency primitives. Left: child_process — main Node spawns a separate OS process (exec, spawn, fork), communicates via messages, no shared memory. Middle: cluster — a primary forks N identical worker processes that share a port and round-robin incoming connections. Right: worker_threads — main thread spawns in-process threads that share memory via SharedArrayBuffer and ArrayBuffer transfers.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- child_process -->
    <text x="120" y="24" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">child_process</text>
    <rect x="40" y="40" width="80" height="36" rx="6" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="80" y="62" font-size="10" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">main</text>
    <rect x="135" y="40" width="80" height="36" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="175" y="62" font-size="10" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">child</text>
    <line x1="120" y1="58" x2="135" y2="58" stroke="#64748b" stroke-width="1.5"/>
    <text x="120" y="98" font-size="9" fill="#475569" text-anchor="middle">separate OS process</text>
    <text x="120" y="112" font-size="9" fill="#475569" text-anchor="middle">no shared memory · messages</text>
    <text x="120" y="134" font-size="9" font-style="italic" fill="#1e1b4b" text-anchor="middle">run a program, shell out</text>

    <!-- cluster -->
    <text x="370" y="24" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">cluster</text>
    <rect x="290" y="40" width="80" height="36" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="330" y="62" font-size="10" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">primary</text>
    <rect x="390" y="30" width="60" height="22" rx="4" fill="#e0e7ff" stroke="#6366f1" stroke-width="1"/>
    <text x="420" y="45" font-size="9" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">worker</text>
    <rect x="390" y="56" width="60" height="22" rx="4" fill="#e0e7ff" stroke="#6366f1" stroke-width="1"/>
    <text x="420" y="71" font-size="9" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">worker</text>
    <rect x="390" y="82" width="60" height="22" rx="4" fill="#e0e7ff" stroke="#6366f1" stroke-width="1"/>
    <text x="420" y="97" font-size="9" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">worker</text>
    <line x1="370" y1="58" x2="388" y2="45" stroke="#64748b" stroke-width="1"/>
    <line x1="370" y1="58" x2="388" y2="67" stroke="#64748b" stroke-width="1"/>
    <line x1="370" y1="58" x2="388" y2="93" stroke="#64748b" stroke-width="1"/>
    <text x="370" y="134" font-size="9" font-style="italic" fill="#052e16" text-anchor="middle">N workers share one port</text>

    <!-- worker_threads -->
    <text x="620" y="24" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">worker_threads</text>
    <rect x="540" y="40" width="80" height="36" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="580" y="62" font-size="10" font-family="ui-monospace, monospace" fill="#500724" text-anchor="middle">main thread</text>
    <rect x="635" y="30" width="65" height="22" rx="4" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <text x="667" y="45" font-size="9" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">worker</text>
    <rect x="635" y="56" width="65" height="22" rx="4" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <text x="667" y="71" font-size="9" font-family="ui-monospace, monospace" fill="#422006" text-anchor="middle">worker</text>
    <line x1="620" y1="58" x2="633" y2="41" stroke="#64748b" stroke-width="1"/>
    <line x1="620" y1="58" x2="633" y2="67" stroke="#64748b" stroke-width="1"/>
    <text x="620" y="98" font-size="9" fill="#500724" text-anchor="middle">in-process · shared memory</text>
    <text x="620" y="134" font-size="9" font-style="italic" fill="#500724" text-anchor="middle">offload CPU work</text>
  </g>
</svg>
```

## 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](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/](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/](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](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](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](https://nodejs.org/api/worker_threads.html#worker-threads)

```quiz
Q: What does PM2 provide that `node server.js` alone does not?
- Faster startup
- Crash recovery (auto-restart), zero-downtime reload, cluster mode, and log/monitoring tooling
correct: 1
explain: PM2 wraps the Node process. If it crashes, PM2 restarts it; reloads swap workers without dropping requests; cluster mode runs one worker per core; and it aggregates logs and metrics. Plain node does none of this.

Q: Which concurrency primitive is best for offloading heavy CPU-bound JavaScript (image processing, hashing) without blocking the event loop?
- child_process
- worker_threads
correct: 1
explain: worker_threads run within the same process and can share memory, making them cheaper than a full process for CPU-bound JavaScript. child_process is heavier; cluster is for throughput, not for offloading computation.

Q: What is the cluster module's job?
- Run external shell commands from Node
- Fork multiple identical worker processes that share a single port, distributing incoming connections across cores
correct: 1
explain: Cluster lets a primary fork N workers, all listening on the same port. The primary hands connections to workers round-robin, so an N-core machine handles roughly N times the load. PM2 automates this.

Q: Why must user input passed to child_process.exec be sanitized?
- It is slower with user input
- exec runs the command through a shell, so unsanitized user input enables command injection
correct: 1
explain: exec invokes a shell to run the command string. Malicious input can chain shell operators (;, |, &&) to run arbitrary commands. spawn with an argument array avoids the shell and the injection vector.

Q: For I/O-heavy work (database calls, API fan-out), do you need worker_threads?
- Yes — the event loop is not enough
- No — the event loop already handles I/O concurrency; worker_threads are for CPU-bound work
correct: 1
explain: The event loop's non-blocking I/O already gives high concurrency for I/O-bound workloads. worker_threads exist for CPU-bound work that would otherwise occupy the single main thread and stall I/O callbacks.
```
