Node.js Processes and Threads


Node.js Processes and Threads Interview with follow-up questions

1. What is the difference between spawn() and exec() in Node.js?

Both run external commands via child_process, but differ in interface and use case:

  • spawn(cmd, args) — launches the command without a shell by default and returns a ChildProcess whose stdout/stderr are streams. Ideal for long-running processes or large/continuous output, since you consume data incrementally with constant memory.
  • exec(cmd) — runs the command in a shell and buffers all output, delivering it once via a callback (or promisified). Convenient for short commands whose output is small, but the buffer has a maxBuffer limit and will error if exceeded.
import { spawn } from 'node:child_process';
const ls = spawn('ls', ['-la']);
ls.stdout.on('data', (d) => process.stdout.write(d));

The security point interviewers reward: because exec runs through a shell, interpolating untrusted input invites command injection — prefer spawn/execFile with an args array (no shell) for anything involving user input. Round it out by naming the siblings: execFile (like exec but no shell), and fork (spawn a new Node process with an IPC channel for message passing).

↑ Back to top

Follow-up 1

When would you use spawn() over exec()?

You would use spawn() over exec() in the following scenarios:

  • When you need to interact with the command in real-time, sending input and receiving output as it happens.
  • When you are running a long-running process that produces a large amount of output.
  • When you want to stream the output of the command to another process or file.

For example, if you need to run a command that continuously outputs data, such as a server process, you would use spawn() to capture the output in real-time and process it as needed.

Follow-up 2

Can you explain how memory management differs between the two?

In terms of memory management, spawn() and exec() behave differently:

  • spawn() uses a streaming interface for both input and output, which means that it can handle large amounts of data without consuming excessive memory. It streams the output directly to the parent process or to a file, without buffering it in memory.

  • exec() buffers the output of the command in memory, which means that it can consume a significant amount of memory if the output is large. The entire output is stored in a buffer and can be accessed once the command has finished executing.

Therefore, if memory usage is a concern and you are dealing with large amounts of output, spawn() is generally more memory-efficient than exec().

Follow-up 3

What are the limitations of exec()?

The exec() function in Node.js has a few limitations:

  • Since exec() runs the command in a shell, it is subject to the limitations and behavior of the shell environment. This means that certain shell-specific features or syntax may not work as expected.

  • exec() buffers the output of the command, which means that if the output is very large, it can consume a significant amount of memory. This can lead to performance issues or even out-of-memory errors.

  • exec() does not provide a streaming interface for input and output, so you cannot interact with the command in real-time. You can only access the output once the command has finished executing.

If you need to overcome these limitations, you should consider using spawn() instead of exec().

2. How does Node.js handle child processes?

Node manages child processes through the built-in child_process module, which lets you launch separate OS processes, stream their I/O, and communicate with them. The four methods, and when to use each:

  • spawn — run a command with streamed stdio; best for long-running or large-output processes.
  • exec — run a command in a shell and buffer the output; convenient for short commands (mind injection risk and maxBuffer).
  • execFile — like exec but no shell; safer for untrusted args.
  • fork — spawn a new Node.js process running a module, with a built-in IPC channel (child.send() / process.on('message')).
import { fork } from 'node:child_process';
const child = fork('./worker.js');
child.send({ job: 1 });
child.on('message', (res) => console.log(res));

Key points: child processes are separate OS processes with their own memory (unlike worker_threads, which are threads sharing memory in one process) — use them for running external programs or isolating crashy/CPU-heavy work, and fork for parallel Node scripts. Always handle 'error'/'exit' and avoid shell interpolation of user input.

↑ Back to top

Follow-up 1

What are the different ways to create child processes in Node.js?

There are three different ways to create child processes in Node.js:

  1. spawn(): This method is used to spawn a new process and provides a streaming interface for I/O.

  2. exec(): This method is used to execute a command in a shell and buffers the output.

  3. fork(): This method is a variation of spawn() that creates a new Node.js process and establishes a communication channel between the parent and child process.

Follow-up 2

Can you explain how data communication happens between parent and child processes?

Data communication between parent and child processes in Node.js can be done through the use of inter-process communication (IPC). The child_process module provides several methods to facilitate this communication:

  1. send(): This method is used to send a message from the parent process to the child process.

  2. on('message'): This event is emitted in the child process when a message is received from the parent process.

  3. stdout and stdin: These streams can be used for standard input and output communication between the parent and child processes.

Follow-up 3

What happens when a child process crashes?

When a child process crashes in Node.js, it emits the exit event. You can listen to this event using the on('exit') method. Additionally, you can also listen to the error event to handle any errors that occur during the execution of the child process. If a child process crashes, it does not affect the parent process or any other child processes that may be running.

3. What is the role of the cluster module in Node.js?

The cluster module lets you fork multiple Node processes (workers) that share the same server port, so an otherwise single-threaded server can use all CPU cores. The OS/primary process load-balances incoming connections across the workers.

import cluster from 'node:cluster';
import { cpus } from 'node:os';
import http from 'node:http';

if (cluster.isPrimary) {
  for (let i = 0; i < cpus().length; i++) cluster.fork();
  cluster.on('exit', () => cluster.fork());   // restart dead workers
} else {
  http.createServer((req, res) => res.end('ok')).listen(3000);
}

Points worth making: each worker is a separate process with its own memory and event loop (so no shared state — use Redis/a DB for shared session/cache), and clustering improves throughput and resilience (a crashed worker can be restarted) for I/O-bound servers.

The current nuance: in production, people often let the platform do this — running N container replicas behind a load balancer (Kubernetes), or a process manager like PM2 in cluster mode — rather than hand-coding cluster. And cluster is for scaling I/O across cores; for CPU-bound tasks within a process you use worker_threads instead.

↑ Back to top

Follow-up 1

How does the cluster module help in improving the performance of a Node.js application?

The cluster module helps in improving the performance of a Node.js application by utilizing multiple CPU cores. By creating a cluster of worker processes, each running on a separate core, the application can handle a higher number of concurrent requests. This allows for better utilization of the available hardware resources and can significantly improve the application's throughput and response time.

Follow-up 2

Can you explain how the master and worker processes work in the cluster module?

In the cluster module, the master process is responsible for creating and managing the worker processes. It listens for incoming connections and distributes them among the workers using a round-robin algorithm. The worker processes are the actual instances of the Node.js application that handle the incoming requests. They communicate with the master process using inter-process communication (IPC) channels.

Follow-up 3

What are the limitations of the cluster module?

The cluster module has a few limitations. First, it does not automatically scale the number of worker processes based on the system load. This means that if the application experiences a sudden increase in traffic, it may not be able to handle the load efficiently. Second, the cluster module does not provide built-in support for sharing server-side state between worker processes. Third, the cluster module does not handle process crashes or restarts automatically, requiring additional logic to handle these scenarios.

4. How does Node.js handle multi-threading?

Your JavaScript runs on a single main thread, but Node gives you real parallelism when you need it:

  • worker_threads — the primary answer for CPU-bound work. Each worker is a real thread with its own V8 isolate and event loop, running inside the same process. They communicate by message passing (postMessage) and can share memory via SharedArrayBuffer. Use them to offload heavy computation (parsing, image/crypto work) so the main loop stays responsive.
  • cluster / child processes — multiple processes (separate memory) to scale I/O-bound servers across cores or run external programs.
  • libuv thread pool — already multi-threaded under the hood for some async I/O (fs, DNS, crypto), transparent to you.
import { Worker } from 'node:worker_threads';
const w = new Worker('./heavy.js', { workerData: input });
w.on('message', (result) => console.log(result));

The framing interviewers want: Node is single-threaded for your JS by design, and you reach for worker_threads for CPU-bound parallelism (shared memory, lighter than processes) and cluster/processes for scaling I/O across cores (isolated memory). A common pattern is a worker pool (e.g. Piscina) to reuse threads rather than spawning one per task.

↑ Back to top

Follow-up 1

Can you explain how the worker_threads module works?

The worker_threads module in Node.js allows you to create and manage worker threads, which are separate threads that can execute JavaScript code in parallel. This module provides a Worker class that can be used to create new worker threads. Here's an example of how to use the worker_threads module:

const { Worker } = require('worker_threads');

const worker = new Worker('./worker.js');

worker.on('message', (message) => {
  console.log('Received message from worker:', message);
});

worker.postMessage('Hello from main thread!');

Follow-up 2

What are the differences between child processes and worker threads?

Child processes and worker threads are both ways to handle multi-threading in Node.js, but they have some differences:

  • Child processes are separate instances of the Node.js process that can run independently and communicate with each other through inter-process communication (IPC). They are useful for running CPU-intensive tasks or executing external commands.

  • Worker threads, on the other hand, are lightweight threads that can execute JavaScript code in parallel within the same Node.js process. They are useful for running CPU-intensive tasks or performing parallel computations.

In summary, child processes are separate instances of the Node.js process, while worker threads are lightweight threads within the same process.

Follow-up 3

When would you use worker threads over child processes?

You would use worker threads over child processes in the following scenarios:

  • When you need to perform parallel computations or run CPU-intensive tasks within the same Node.js process.

  • When you want to avoid the overhead of creating separate instances of the Node.js process for each task.

  • When you need to share memory and state between multiple threads, as worker threads can share memory with the main thread.

However, it's important to note that worker threads are still an experimental feature in Node.js, so they may not be as stable or well-supported as child processes.

5. What is the event loop in Node.js and how does it handle asynchronous operations?

The event loop is what lets single-threaded Node perform non-blocking I/O: it dispatches completed async operations to their callbacks (across ordered phases) instead of waiting on them, so the main thread stays free. When you start an async operation, Node registers the callback and moves on; libuv/the OS does the waiting, and the loop runs the callback when the result is ready.

import { readFile } from 'node:fs';

readFile('file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);            // runs later, after I/O completes
});

console.log('This runs first.'); // synchronous, runs immediately

Output: This runs first. then the file contents — because readFile is non-blocking, the synchronous console.log runs first while the read happens in the background.

The detail that earns marks: between phases the loop drains microtasksprocess.nextTick first, then promise callbacks — so await/.then() continuations run before setTimeout/setImmediate. And the golden rule: keep CPU-heavy work off the loop (use worker_threads), since it would block every pending callback. Modern code typically uses fs/promises + async/await for the same behavior with cleaner syntax.

↑ Back to top

Follow-up 1

How does the event loop handle blocking operations?

The event loop in Node.js is designed to handle non-blocking operations efficiently. However, if a blocking operation is encountered, it can potentially block the event loop and prevent other operations from being processed. To mitigate this issue, Node.js provides several mechanisms:

  1. Delegating blocking operations to worker threads: Node.js allows you to offload blocking operations to worker threads using the worker_threads module. This way, the event loop remains free to handle other operations while the blocking operation is being executed in a separate thread.

  2. Using asynchronous versions of blocking operations: Node.js provides asynchronous versions of many blocking operations, such as fs.readFile instead of fs.readFileSync. These asynchronous versions allow the event loop to continue processing other operations while waiting for the blocking operation to complete.

By using these mechanisms, the event loop can effectively handle blocking operations without getting blocked itself.

Follow-up 2

Can you explain the role of the call stack in the event loop?

The call stack is a data structure used by the event loop to keep track of function calls in a program. When a function is called, a new frame is pushed onto the call stack to store the function's arguments, local variables, and return address. The event loop uses the call stack to determine which function is currently being executed.

In the context of the event loop, the call stack plays a crucial role in handling asynchronous operations. When an asynchronous operation is initiated, its callback function is registered and the event loop continues executing the remaining code. Once the operation is complete and the callback function is picked up by the event loop, it is pushed onto the call stack for execution.

It's important to note that the call stack has a limited capacity. If the call stack becomes too large, it can result in a stack overflow error. To prevent this, Node.js uses a technique called tail call optimization to optimize recursive function calls and reduce the stack size.

Follow-up 3

What is the role of the task queue in the event loop?

The task queue, also known as the callback queue or message queue, is a data structure used by the event loop to store callback functions that are ready to be executed. When an asynchronous operation is complete and its callback function is ready to be executed, it is added to the task queue.

The event loop continuously checks the task queue for any pending callback functions. If the call stack is empty, meaning there are no functions currently being executed, the event loop will pick up the next callback function from the task queue and push it onto the call stack for execution.

The task queue ensures that callback functions are executed in the order they were added, following the principle of FIFO (First-In-First-Out). This allows Node.js to maintain the asynchronous nature of its operations and ensure that callbacks are executed as soon as possible.

Live mock interview

Mock interview: Node.js Processes and Threads

Intermediate ~5 min Your own free AI key

Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.