# Child Processes, Worker Threads and Clustering

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: Networking and Parallelism
Source: https://www.devtutorials.ai/learn/nodejs/network-parallel/child-processes-and-workers

> When to use parallelism, recognising CPU-bound work, child processes with exec, execFile, spawn and fork, options, streaming output, IPC, killing and detaching processes and handling their errors, worker threads with workerData, parentPort, message passing, structured clone, transferable objects, errors, termination and worker pools, SharedArrayBuffer and Atomics, MessageChannel and resource limits, the cluster module, and choosing between child processes, worker threads and cluster.

## Child Processes, Worker Threads and Clustering

**Parallelism** = doing several jobs at the very same moment, on different CPU cores.

- Your JavaScript runs on one thread (see **Event Loop and Timers**).
- Modern computers have 4, 8 or more cores. One thread uses only one.
- Node.js can start **child processes**, **worker threads** and **cluster** workers.
- Each one runs on its own, so heavy work stops blocking your main program.

**Simple lesson:** when one thread is not enough, give heavy work its own thread or process.

### A Real-World Example

Think of exam correction at Greenfield School.

- One teacher corrects 400 papers. It takes all night. Nobody can ask her anything.
- Better: the principal gives 100 papers to each of 4 teachers in other rooms.
- Each teacher works alone and sends back a list of marks.
- The principal stays free to answer parents at the door.

The principal is the main thread. The other teachers are workers. The mark lists are **messages**.

## What You Will Learn

In this lesson, you will learn:

1. How to recognise CPU-bound work
2. Child processes: `exec`, `execFile`, `spawn` and `fork`
3. Options, streaming output, IPC and killing processes
4. Worker threads: `Worker`, `workerData` and `parentPort`
5. Message passing, structured clone and transferable objects
6. Worker errors, termination and worker pools
7. `SharedArrayBuffer`, `Atomics`, `MessageChannel` and resource limits
8. The `cluster` module
9. How to choose the right approach

## When to Use Parallelism

**CPU-bound work** = work that keeps the processor busy, with no waiting.

- Counting primes, image resizing, PDF making, heavy maths, compressing big files.
- While it runs, timers, requests and events all wait.
- Measure first: if a job takes more than about 50 ms, move it off the main thread.
- I/O-bound work, such as reading files, does **not** need this. Node.js already handles it.

Measure first with `performance.now()`. Move work to a worker only when it really blocks.

**Simple lesson:** if heavy maths makes timers run late, move it to a worker.

## Child Processes

**Child process** = another program started by your Node.js program.

- It can be any program: `git`, `ffmpeg`, Python, or another Node.js script.
- It has its own memory and its own stdin, stdout and stderr.
- `node:child_process` gives four main ways to start one.

<div className="tscroll">

| Function | Runs | Output | Use it for |
|---|---|---|---|
| `exec(command)` | A command through the shell | Buffered, given at the end | Short commands with small output |
| `execFile(file, args)` | A program directly, no shell | Buffered | Safer: arguments are not read by a shell |
| `spawn(file, args)` | A program directly | Streamed as it comes | Long jobs and big output |
| `fork(script)` | A Node.js script | Streamed, plus a message channel | Node.js helpers that talk back |

</div>

```js
// main.mjs
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

// execFile: no shell, arguments passed as a list (safer)
const { stdout } = await execFileAsync("node", ["--version"]);
console.log("Child said:", stdout.trim().startsWith("v") ? "a Node.js version" : stdout);

// A failing command gives an error with the exit code
try {
  await execFileAsync("node", ["-e", "process.exit(3)"]);
} catch (err) {
  console.log("Child failed with code", err.code);
}
```

Output:

```text
Child said: a Node.js version
Child failed with code 3
```

### spawn and Streaming Output

- `spawn` gives you `child.stdout` and `child.stderr` as **streams**.
- Output arrives while the child is still running.
- The `close` event gives the **exit code**.

```js
// main.mjs
import { spawn } from "node:child_process";

const child = spawn("node", ["-e", "for (let i = 1; i <= 3; i++) console.log('page', i)"]);

child.stdout.on("data", (chunk) => {
  // A chunk can hold several lines, so split it.
  for (const line of chunk.toString().trim().split("\n")) console.log("printer:", line);
});
child.on("close", (code) => console.log("Printer finished with code", code));
```

Output:

```text
printer: page 1
printer: page 2
printer: page 3
Printer finished with code 0
```

### Options

- **`cwd`** = the folder the child starts in.
- **`env`** = its environment variables. Pass `{ ...process.env, EXTRA: "1" }` to add one.
- **`shell: true`** = run through a shell. Needed for pipes and `*`. Dangerous with user input.
- **`stdio: "inherit"`** = the child writes straight to your terminal.
- **`timeout`** = kill the child if it runs too long.
- **`signal`** = an `AbortSignal` that kills the child when aborted.

### fork and IPC

**IPC** (inter-process communication) = sending messages between processes.

- `fork(script)` starts a Node.js script with a built-in message channel.
- The parent calls `child.send(data)`. The child listens with `process.on("message")`.
- The child answers with `process.send(data)`.

```js
// main.mjs: one file plays both parts, chosen by an argument
import { fork } from "node:child_process";

if (process.argv[2] === "teacher") {
  // the child: correct one paper and send the mark back
  process.on("message", (paper) => {
    process.send({ name: paper.name, marks: paper.answers.filter(Boolean).length * 10 });
    process.exit(0);
  });
} else {
  const teacher = fork(import.meta.filename, ["teacher"]);
  teacher.on("message", (result) => console.log("Mark from child:", result));
  teacher.on("exit", (code) => console.log("Child exited with code", code));
  teacher.send({ name: "Asha", answers: [true, true, false, true] });
}
```

Output:

```text
Mark from child: { name: 'Asha', marks: 30 }
Child exited with code 0
```

### Killing, Detaching and Errors

- **`child.kill("SIGTERM")`** = ask the child to stop.
- **`detached: true`** with **`child.unref()`** = let the child keep running after the parent exits.
- **`error` event** = the child could not even start, for example the program does not exist (`ENOENT`).
- **`exit` or `close` with a non-zero code** = it started, but failed.

```js
// main.mjs
import { spawn } from "node:child_process";

const missing = spawn("no-such-program");
missing.on("error", (err) => console.log("Could not start:", err.code));

const slow = spawn("node", ["-e", "setTimeout(() => {}, 10000)"]);
slow.on("exit", (code, signal) => console.log("Slow child stopped by", signal));
setTimeout(() => slow.kill("SIGTERM"), 100);
```

Output:

```text
Could not start: ENOENT
Slow child stopped by SIGTERM
```

Never put user input into `exec` or `shell: true`. A name like `x; rm -rf /` would run as a command. Use `execFile` or `spawn` with an argument list. **Cryptography and Security** covers this.

**Simple lesson:** `execFile` for short commands, `spawn` for streams, `fork` for Node.js helpers with messages.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 180" role="img" ariaLabelledby="fig-node-spawn" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-spawn">Animation: the main program starts a child process and reads its output through a pipe</title>
<rect x="20" y="50" width="170" height="80" rx="12"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="105" y="82" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>your Node.js app</text><text x="105" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>spawn("git", ["log"])</text>
<rect x="370" y="50" width="170" height="80" rx="12"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="455" y="82" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>child process</text><text x="455" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>its own memory and PID</text>
<line x1="190" y1="72" x2="370" y2="72"  style={{ stroke: "var(--ink)" }}/><line x1="190" y1="108" x2="370" y2="108"  style={{ stroke: "var(--ink)", strokeDasharray: "4 4" }}/>
<text x="280" y="64" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }}>1. start it</text><text x="280" y="126" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }}>2. output and exit code</text>
<circle r="6" cx="190" cy="72" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="190;370;370" keyTimes="0;0.35;1" dur="6s" repeatCount="indefinite" /></circle>
<circle r="6" cx="370" cy="108" opacity="0" style={{ fill: "var(--ngc-green)" }}><animate attributeName="cx" values="370;370;190;190" keyTimes="0;0.45;0.8;1" dur="6s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.46;0.8;0.81" dur="6s" repeatCount="indefinite" /></circle>
<text x="280" y="164" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }}>They share nothing. They only talk through pipes and messages.</text>
</svg>
<figcaption>A child process is a separate program. Data travels between you through pipes.</figcaption>
</figure>

## Worker Threads

**Worker thread** = another JavaScript thread inside the same process, with its own event loop.

- Create it with `new Worker(file, { workerData })` from `node:worker_threads`.
- **`workerData`** = data the worker gets at the start.
- **`parentPort`** = the worker's line back to the main thread.
- Workers share the process, so they start faster and use less memory than child processes.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 210" role="img" ariaLabelledby="fig-node-workers" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-workers">Animation: the main thread sends jobs to two worker threads through message ports and gets results back, while it keeps answering requests</title>
<rect x="20" y="70" width="170" height="70" rx="10"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }}/><text x="105" y="98" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Main thread</text><text x="105" y="118" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>keeps answering requests</text>
<rect x="370" y="20" width="170" height="60" rx="10"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="455" y="46" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Worker 1</text><text x="455" y="65" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>counts primes</text>
<rect x="370" y="130" width="170" height="60" rx="10"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="455" y="156" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Worker 2</text><text x="455" y="175" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>resizes photos</text>
<line x1="190" y1="95" x2="370" y2="50"  style={{ stroke: "var(--ngc-slate)", strokeDasharray: "4 4" }}/><line x1="190" y1="115" x2="370" y2="160"  style={{ stroke: "var(--ngc-slate)", strokeDasharray: "4 4" }}/>
<circle r="6" cx="190" cy="95" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="190;370;370;190" keyTimes="0;0.3;0.6;1" dur="4s" repeatCount="indefinite" /><animate attributeName="cy" values="95;50;50;95" keyTimes="0;0.3;0.6;1" dur="4s" repeatCount="indefinite" /></circle>
<circle r="6" cx="190" cy="115" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="cx" values="190;370;370;190" keyTimes="0;0.4;0.75;1" dur="4s" repeatCount="indefinite" /><animate attributeName="cy" values="115;160;160;115" keyTimes="0;0.4;0.75;1" dur="4s" repeatCount="indefinite" /></circle>
<text x="280" y="205" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>postMessage out, message event back</text>
</svg>
<figcaption>Workers do heavy work in parallel and talk to the main thread with messages.</figcaption>
</figure>

```js
// main.mjs: the same file is the main thread and the worker
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";

function countPrimes(limit) {
  let count = 0;
  for (let n = 2; n < limit; n++) {
    let prime = true;
    for (let d = 2; d * d <= n; d++) if (n % d === 0) { prime = false; break; }
    if (prime) count++;
  }
  return count;
}

if (isMainThread) {
  const worker = new Worker(new URL(import.meta.url), { workerData: { limit: 300000 } });
  worker.on("message", (n) => console.log("Worker found", n, "primes"));
  worker.on("error", (err) => console.log("Worker failed:", err.message));
  worker.on("exit", (code) => console.log("Worker exited with code", code));
  console.log("Main thread is free while the worker counts");
} else {
  parentPort.postMessage(countPrimes(workerData.limit));
}
```

Output:

```text
Main thread is free while the worker counts
Worker found 25997 primes
Worker exited with code 0
```

### Message Passing and Structured Clone

**Structured clone** = the way messages are copied between threads.

- Objects, arrays, `Map`, `Set`, `Date` and `Buffer` copy fine.
- **Functions** and class methods cannot be sent. You get a `DataCloneError`.
- Each side gets its own copy. Changing one does not change the other.

### Transferable Objects

- Copying a 100 MB buffer takes time and memory.
- **Transfer** = hand the memory over instead of copying it. Pass it in the transfer list.
- After the transfer, the sender's buffer is **empty** (byteLength 0).

### Worker Errors, Termination and Pools

- An uncaught error in a worker fires the **`error`** event on the `Worker` object. The main thread keeps running.
- **`worker.terminate()`** = stop a worker now.
- Starting a worker takes time and memory. Do not start one per task.
- **Worker pool** = a fixed number of workers (often one per core) that take tasks from a queue.
- Libraries such as `piscina` give you a ready pool.

Browsers have Web Workers with the same idea: heavy work on another thread, and messages to talk.

- Four papers start at once. Papers 5 and 6 wait in the queue for the first free workers.
- A real pool does the same with real `Worker` objects.

### SharedArrayBuffer, Atomics and MessageChannel (Advanced)

- **`SharedArrayBuffer`** = memory that several threads see at the same time. No copying at all.
- **`Atomics`** = safe operations on shared memory, such as `Atomics.add`, so two threads do not overwrite each other.
- **`MessageChannel`** = a pair of connected ports. Give one port to a worker for a private line.
- **Resource limits** = `new Worker(file, { resourceLimits: { maxOldGenerationSizeMb: 64 } })` caps a worker's memory.

```js
// main.mjs: four workers add to one shared counter safely
import { Worker, isMainThread, workerData } from "node:worker_threads";

if (isMainThread) {
  const shared = new SharedArrayBuffer(4);
  const counter = new Int32Array(shared);
  let finished = 0;
  for (let i = 0; i < 4; i++) {
    const w = new Worker(new URL(import.meta.url), { workerData: shared });
    w.on("exit", () => {
      if (++finished === 4) console.log("Counter:", Atomics.load(counter, 0), "(4 workers x 1000)");
    });
  }
} else {
  const counter = new Int32Array(workerData);
  for (let i = 0; i < 1000; i++) Atomics.add(counter, 0, 1);
}
```

Output:

```text
Counter: 4000 (4 workers x 1000)
```

**Simple lesson:** workers share a process, talk by copied messages, and can share memory carefully with Atomics.

## The cluster Module

**cluster** = start several copies of your server, one per CPU core, all sharing one port.

- The **primary** process starts the copies with `cluster.fork()`.
- Each **worker** process runs the same server code.
- Incoming connections are shared between the workers.
- If a worker crashes, the primary can start a new one.

```js
// main.mjs
import cluster from "node:cluster";
import http from "node:http";
import os from "node:os";

if (cluster.isPrimary) {
  const count = Math.min(2, os.availableParallelism());
  console.log(`Primary starting ${count} workers`);
  let ready = 0;
  for (let i = 0; i < count; i++) cluster.fork();
  cluster.on("listening", () => {
    if (++ready === count) {
      console.log("All workers share port 8123");
      for (const w of Object.values(cluster.workers)) w.kill();
    }
  });
  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.id} stopped (${signal ?? code})`);
    // In a real server: cluster.fork() here to replace a crashed worker.
  });
} else {
  http.createServer((req, res) => res.end(`Hello from worker ${process.pid}`)).listen(8123);
}
```

Output:

```text
Primary starting 2 workers
All workers share port 8123
Worker 1 stopped (SIGTERM)
Worker 2 stopped (SIGTERM)
```

### Cluster vs a Reverse Proxy

- Today, many apps run **one process per container** and let the platform start several copies.
- A **reverse proxy** or **load balancer** (such as Nginx or a cloud balancer) shares traffic between them.
- Process managers such as **PM2** can also run a cluster for you.
- Use `cluster` directly only on a single server without such tools.

**Simple lesson:** `cluster` copies a whole server per core, but containers and balancers often do this job now.

## Choosing an Approach

<div className="tscroll">

| Need | Child process | Worker thread | Cluster |
|---|---|---|---|
| Run another program (git, Python) | Yes | No | No |
| Heavy JavaScript maths without blocking | Works, but heavy | Yes, best choice | No |
| Use every core for one web server | No | No | Yes |
| Share memory | No | Yes, with `SharedArrayBuffer` | No |
| Start-up cost | High | Medium | High |
| A crash affects | Only the child | The worker (handled by `error`) | Only that worker process |

</div>

**Simple lesson:** other programs → child process, heavy JS → worker thread, more server copies → cluster.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Using workers for file reading | More threads make I/O faster | I/O is already non-blocking | Extra cost, no gain |
| `exec` with user input | The shell is harmless | Use `execFile` or `spawn` with an argument list | Command injection |
| One new Worker per request | Workers are free | Use a pool | Slow start-up and memory blowing up |
| Sending a function in `postMessage` | Everything can be sent | Send plain data | `DataCloneError` |
| No `error` listener on a child or worker | Failures are printed | Listen for `error` and `exit` | Silent failures or crashes |
| Using a transferred buffer again | Transfer copies | Transfer moves the memory | The sender's buffer is empty |
| Big output through `exec` | Output size is unlimited | Use `spawn` and streams | `maxBuffer` error |

</div>

## TL;DR

1. **CPU-bound** = keeps the processor busy and blocks the event loop.
2. **`exec`** = shell command, buffered. **`execFile`** = no shell, safer.
3. **`spawn`** = streams output. **`fork`** = Node.js script with `send` and `message`.
4. **Worker thread** = another JS thread. `workerData` in, `parentPort.postMessage` out.
5. **Structured clone** = messages are copied. Functions cannot be sent.
6. **Transfer** = move memory instead of copying it.
7. **Worker pool** = a few reusable workers and a queue.
8. **cluster** = one server process per core, sharing a port.

## Check Yourself

## Exercises

## What to Remember

- Move CPU-bound work off the main thread. Leave I/O where it is.
- `execFile` and `spawn` run other programs. Never pass user input to a shell.
- `fork` starts a Node.js child with a message channel.
- Worker threads run heavy JavaScript and talk with copied messages.
- Transfer big buffers instead of copying them, and reuse workers in a pool.
- `cluster` runs server copies, but containers and balancers often do it today.

Next: **Testing with the Built-in Test Runner**. You will check your code automatically with `node:test`.
