Node.js runs your JavaScript on a single thread, which is perfect for I/O such as files, databases and network calls: while one request waits, others are served. But some work isn't waiting, it's computing: resizing images, generating a large PDF, hashing thousands of passwords, parsing a huge file. While that runs, the event loop is blocked, and every other request on the server has to wait.
Node offers two ways to escape the single thread. Child processes run a separate program, which might be another Node script or any command-line tool such as ffmpeg, git or ImageMagick. Worker threads run JavaScript in parallel threads inside the same process, ideal for CPU-heavy calculations in your own code. For using every CPU core to serve web traffic, the cluster module, or simply running several instances behind a load balancer, does the job.
In this lesson you'll run commands safely and stream their output, move a slow calculation into a worker thread and see the main thread stay responsive, build a small worker pool, and learn which tool fits which problem.
Running other programs
import { execFile, spawn } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%s']);
const child = spawn('ffmpeg', ['-i', 'in.mov', 'out.mp4']);
child.stderr.on('data', (chunk) => process.stdout.write(chunk));
child.on('close', (code) => console.log(`Exited with code ${code}`));execFileruns a program and collects its output when it finishes; good for short commands with small output.spawnstreams output as it's produced; good for long-running or chatty programs.- Pass arguments as an array. Avoid
execwith user input: it runs through a shell, where characters like;and&&can inject extra commands. forkstarts another Node script with a built-in message channel between parent and child.
Worker threads
// main.js
import { Worker } from 'node:worker_threads';
const worker = new Worker(new URL('./primes.js', import.meta.url), { workerData: { limit: 5_000_000 } });
worker.on('message', (count) => console.log(`Found ${count} primes`));
worker.on('error', (err) => console.error(err));// primes.js
import { parentPort, workerData } from 'node:worker_threads';
parentPort.postMessage(countPrimes(workerData.limit));- Each worker has its own event loop and memory; data is copied between threads with
postMessage. - Large binary data can be transferred or shared with
SharedArrayBufferto avoid copying. - Creating a worker takes time and memory, so long-running servers keep a pool of workers and reuse them. Libraries such as Piscina do this for you.
Cluster and scaling across cores
A single Node process uses one CPU core for JavaScript. To use all cores for web traffic, run several processes. The built-in cluster module forks workers that share one port; in production it's more common to let the platform handle it, such as several containers behind a load balancer, PM2's cluster mode, or a platform's instance count. Each process has its own memory, so shared state such as sessions and caches must live in Redis or the database.
Node.js Advanced Syntax
Processes and workers
import { execFile, spawn, fork } from 'node:child_process';
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
const { stdout } = await promisify(execFile)('node', ['--version']);
const p = spawn('npm', ['run', 'build']); p.on('close', (code) => {});
const w = new Worker(new URL('./job.js', import.meta.url), { workerData: input });
w.on('message', handle); w.postMessage({ type: 'start' });
parentPort.postMessage(result); // inside the worker
import cluster from 'node:cluster'; if (cluster.isPrimary) cluster.fork();Node.js Advanced Summary
- CPU-heavy JavaScript blocks the event loop and delays every other request.
- execFile runs a command and collects output; spawn streams output; fork runs Node scripts.
- Pass command arguments as arrays and avoid exec with user input.
- Worker threads run JavaScript in parallel; communicate with workerData and postMessage.
- Use cluster or multiple instances to serve traffic on every CPU core.
Node.js Advanced Examples
Each example shows the code, the output it produces and what each part does.
Running a command with execFile and spawn
import { execFile, spawn } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile);
const { stdout } = await run(process.execPath, ['-e', 'console.log(6 * 7)']);
console.log(`Child said: ${stdout.trim()}`);
try {
await run(process.execPath, ['-e', 'process.exit(3)']);
} catch (err) {
console.log(`Failed with exit code ${err.code}`);
}
const child = spawn(process.execPath, ['-e', "for (let i = 1; i <= 3; i++) console.log('step', i)"]);
let lines = 0;
child.stdout.on('data', (chunk) => { lines += chunk.toString().trim().split('\n').length; });
child.on('close', (code) => console.log(`spawn finished: code ${code}, ${lines} lines`));Child said: 42
Failed with exit code 3
spawn finished: code 0, 3 linesLine-by-line explanation
- Line 6process.execPath is the path to Node itself; any program could be run the same way, with arguments passed as an array, never through a shell.
- Lines 9 to 12A non-zero exit code makes the promise reject, with the code on the error.
- Lines 15 to 17spawn streams output as it arrives and reports the exit code on close.
Keeping the event loop responsive with a worker
// count-primes.mjs
import { 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;
}
parentPort.postMessage(countPrimes(workerData.limit));// main.mjs
import { Worker } from 'node:worker_threads';
let ticks = 0;
const timer = setInterval(() => ticks++, 10);
const worker = new Worker(new URL('./count-primes.mjs', import.meta.url), { workerData: { limit: 2_000_000 } });
worker.on('message', (count) => {
clearInterval(timer);
console.log(`Primes up to 2,000,000: ${count}`);
console.log(`Main thread stayed responsive: ${ticks > 5}`);
});
worker.on('error', (err) => console.error(err));Primes up to 2,000,000: 148933
Main thread stayed responsive: trueLine-by-line explanation
- Line 2The worker reads its input from workerData.
- Lines 4 to 12A deliberately CPU-heavy calculation that would block a server for a noticeable time.
- Line 14postMessage sends the result back to the main thread. Because the work runs in a worker, the main thread's timer keeps ticking the whole time, just as a server would keep answering requests.
A small worker pool
// hash-worker.mjs
import { parentPort } from 'node:worker_threads';
import { scryptSync } from 'node:crypto';
parentPort.on('message', ({ id, password }) => {
const hash = scryptSync(password, 'demo-salt', 32).toString('hex');
parentPort.postMessage({ id, length: hash.length });
});// pool.mjs
import { Worker } from 'node:worker_threads';
import { availableParallelism } from 'node:os';
const size = Math.min(4, availableParallelism());
const workers = Array.from({ length: size }, () => new Worker(new URL('./hash-worker.mjs', import.meta.url)));
const pending = new Map();
workers.forEach((w) => w.on('message', ({ id, length }) => { pending.get(id)(length); pending.delete(id); }));
let next = 0;
function hash(password) {
const id = next++;
const worker = workers[id % workers.length];
return new Promise((resolve) => {
pending.set(id, resolve);
worker.postMessage({ id, password });
});
}
const passwords = Array.from({ length: 12 }, (_, i) => `password-${i}`);
const lengths = await Promise.all(passwords.map(hash));
console.log(`Hashed ${lengths.length} passwords, each ${lengths[0]} hex characters`);
console.log(`Pool size between 1 and 4: ${size >= 1 && size <= 4}`);
await Promise.all(workers.map((w) => w.terminate()));Hashed 12 passwords, each 64 hex characters
Pool size between 1 and 4: trueLine-by-line explanation
- Line 5Each worker keeps running and handles many messages, so the startup cost is paid once.
- Line 6scryptSync is slow on purpose; in a worker it doesn't block the main thread.
- Line 7The id lets the main thread match each answer to the right request.
Common Node.js Advanced Mistakes
Heavy computation on the main thread
app.get('/report', (req, res) => {
res.json(buildHugeReport()); // every other request waits until this finishes
});app.get('/report', async (req, res) => {
res.json(await runInWorker('build-report.js', req.query));
});exec with user input
exec(`zip archive.zip ${req.query.file}`); // "a.txt; rm -rf ~" runs a second commandexecFile('zip', ['archive.zip', safeName(req.query.file)]);A new worker for every request
app.post('/hash', (req, res) => {
const w = new Worker('./hash.js', { workerData: req.body }); // slow to start, memory grows under load
});const pool = new Piscina({ filename: new URL('./hash.js', import.meta.url).href });
app.post('/hash', async (req, res) => res.json(await pool.run(req.body)));Node.js Advanced in Real Projects
Processes and workers in production
Build tools such as esbuild and Vite spread work across worker threads. Image services use sharp (which uses native threads) or worker pools for resizing, and video platforms spawn ffmpeg. CI tools and deployment scripts run git and docker through spawn. Web APIs scale across cores by running several processes in containers, and long jobs such as reports and exports usually move to a background queue like BullMQ processed by separate worker services.
Node.js Advanced FAQ
What is the difference between worker threads and child processes?
Worker threads run JavaScript inside the same Node process, start faster, and can pass messages cheaply or share memory with SharedArrayBuffer. A child process is a completely separate program, which can be any executable, such as git or ffmpeg, and is fully isolated, so a crash there doesn't take down your app. Use workers for CPU-heavy JavaScript and child processes for running other programs.
How many worker threads should I create?
For CPU-bound work, about one worker per available core is a sensible maximum; os.availableParallelism() tells you how many that is. Many apps leave one core for the main thread, which still handles requests. Creating more workers than cores doesn't speed anything up, because they just take turns on the same processors.
How do I stop a child process that runs too long?
Pass a timeout in milliseconds to execFile or exec: when it runs out, Node kills the child with SIGTERM and the call fails with err.killed set to true. You can also pass signal from an AbortController and call abort() whenever you like, or call child.kill() on a process started with spawn.
Will worker threads make my database queries or API calls faster?
No. Waiting on I/O doesn't use the JavaScript thread, so Node can already run thousands of queries and requests at the same time without workers. Workers help only when your own JavaScript does heavy computation, such as resizing images, parsing huge files or hashing large amounts of data.
Why does spawning npm or a .cmd file fail on Windows?
Commands like npm and npx are .cmd batch files on Windows, not executables, and recent Node versions refuse to run .bat or .cmd files through spawn or execFile unless you pass shell: true, as a protection against command injection. If you do use shell: true, never include user input in the arguments.
Node.js Advanced Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line that sends a result from a worker back to the main thread.
parentPort.(result);
What happens to other requests while a Node server runs a long CPU-heavy loop on the main thread?
Which function streams a child process's output as it's produced?
How does data get into a worker thread when it starts?
Would worker threads speed up an API whose time is spent waiting on database queries?
Node.js Advanced Quiz
5 questions to check what you learned.
Which module provides Worker and parentPort?
- node:child_process
- node:worker_threads
- node:cluster
- node:os
Why pass command arguments as an array to execFile?
- They aren't interpreted by a shell, which prevents command injection
- It's faster
- Arrays are required by Windows
- To run in parallel
What does the cluster module do?
- Groups database rows
- Creates worker threads
- Compresses responses
- Starts several Node processes that share a server port to use more CPU cores
Why use a worker pool rather than a new worker per task?
- Pools are required
- Pools share variables
- Starting workers is slow and uses memory; a pool reuses a fixed set
- Workers can only run once
Is data passed with postMessage shared or copied between threads by default?
- Copied
- Shared
- Deleted
- Encrypted