Sitemap

Understanding Node.js Concurrency: Event Loop, Promises, Worker Threads, Child Processes, and Clusters

4 min readJun 8, 2026

--

When I started learning Node.js concurrency, I had several questions:

  • When should I use Promises vs Worker Threads?
  • Does setTimeout() create a new thread?
  • Why are file operations non-blocking but CPU-heavy calculations block the application?
  • Why can’t Node automatically move heavy computations to another thread?
  • How do Worker Threads communicate?
  • What’s the difference between Worker Threads, Child Processes, and Clusters?

This article summarizes the answers.

Node.js Is Single-Threaded… But Not Exactly

People often say:

“Node.js is single-threaded.”

This is only partially true.

JavaScript execution happens on a single main thread, but Node.js itself uses:

  • Event Loop
  • libuv
  • Thread Pool
  • Operating System APIs

to perform asynchronous work.

Understanding I/O Operations

Consider:

const fs = require('fs');
fs.readFile('data.txt', (err, data) => {
console.log(data.toString());
});
console.log('Done');

Output:

Done
[file contents]

Why doesn’t readFile() block?

Because Node delegates the work.

JavaScript Thread
|
v
libuv
|
v
OS / Thread Pool
|
v
Callback Queue
|
v
Event Loop
|
v
JavaScript Thread

The JavaScript thread asks the OS or libuv to read the file and continues executing other code.

When the file is ready, the callback is pushed back to the Event Loop.

Why Promises Don’t Create Threads

Many developers think:

await fetchData();

creates another thread.

It doesn’t.

A Promise is simply a way to handle asynchronous operations.

For example:

const data = await fetch(url);

The network request is handled by the OS.

The Promise only represents the future result.

Promise
≠
Thread

Promises help with asynchronous I/O, not parallel CPU execution.

Does setTimeout Create a New Thread?

Consider:

setTimeout(() => {
console.log('Hello');
}, 0);

Many beginners assume this runs in a background thread.

It doesn’t.

setTimeout() only schedules a callback.

Flow:

Main Thread
|
| Register Timer
|
v
Timer Queue
|
v
Event Loop
|
v
Main Thread executes callback

The callback eventually runs on the same JavaScript thread.

Why Heavy Calculations Still Block

Example:

setTimeout(() => {
for (let i = 0; i < 1e10; i++) {}
}, 0);

The timer callback executes later, but once it starts running, it still uses the main thread.

Event Loop
|
v
Heavy Calculation
|
v
Everything else waits

This is why setTimeout() is not a solution for CPU-intensive work.

Why Can’t Node Automatically Move Heavy Computation?

Suppose:

let count = 0;
function calculate() {
count++;
}

If Node automatically moved this function to another thread:

Main Thread
count = 0
Worker Thread
count++

Questions arise:

  • Who owns count?
  • What if multiple threads modify it?
  • What if one thread reads while another writes?

These are race conditions.

Node avoids these problems by running JavaScript code on a single thread unless the developer explicitly creates Worker Threads.

Worker Threads

Worker Threads allow JavaScript to run in parallel.

Example:

const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');

Architecture:

Process
|
+-- Main Thread
+-- Worker Thread

Each worker has:

  • Its own V8 engine
  • Its own Event Loop
  • Its own execution context

Worker Threads are ideal for:

  • PDF generation
  • Image processing
  • Video processing
  • Large JSON processing
  • Data analytics
  • Encryption

How Worker Threads Communicate

Most commonly using message passing.

Get prajjwal singh’s stories in your inbox

Join Medium for free to get updates from this writer.

Main Thread:

worker.postMessage(data);

Worker:

parentPort.on('message', (data) => {
// process data
});

Return result:

parentPort.postMessage(result);

Flow:

Main Thread
|
v
Worker Thread
|
v
Main Thread

Shared Memory with SharedArrayBuffer

Normally, Worker Threads do not share JavaScript objects.

Instead, data is copied.

For large numeric datasets, Node provides:

const shared = new SharedArrayBuffer(size);

This allows multiple workers to access the same memory.

Worker 1
\
Shared Memory
/
Worker 2

When multiple workers write to shared memory, Atomics should be used to avoid race conditions.

Processing Large JSON Files

Suppose you have:

10 million records

Instead of processing everything on one thread:

processLargeJson();

Split the work:

Worker 1 -> Records 1-2.5M
Worker 2 -> Records 2.5M-5M
Worker 3 -> Records 5M-7.5M
Worker 4 -> Records 7.5M-10M

Each worker returns a partial result.

Main thread combines them.

This is a common approach for large-scale data processing.

Child Processes

Child Processes are different from Worker Threads.

Example:

const { spawn } = require('child_process');

Architecture:

Main Process
|
+-- Child Process

Each child process has:

  • Separate memory
  • Separate V8 engine
  • Separate Event Loop
  • Separate Process ID (PID)

When to Use Child Processes

Child Processes are useful when running external programs.

Examples:

spawn('python', ['script.py']);
spawn('ffmpeg', ['input.mp4']);
spawn('git', ['clone', repo]);

Worker Threads cannot directly replace these use cases.

Worker Threads vs Child Processes

FeatureWorker ThreadChild ProcessUnitThreadProcessMemory UsageLowerHigherShared MemoryYesNoCommunicationFastIPCRun External ProgramsNoYesStartup CostLowHigherBest ForCPU-heavy JS workRunning external tools

Clusters

Clusters help scale Node.js applications across CPU cores.

Example:

const cluster = require('cluster');

Architecture:

Master Process
|
+-- Worker Process 1
+-- Worker Process 2
+-- Worker Process 3
+-- Worker Process 4

Each worker is actually a separate process.

Clusters are commonly used for web servers.

If a machine has 8 CPU cores:

Core 1 -> Worker 1
Core 2 -> Worker 2
Core 3 -> Worker 3
...
Core 8 -> Worker 8

This allows the server to handle more requests simultaneously.

Worker Threads vs Clusters

FeatureWorker ThreadClusterCreatesThreadsProcessesMemoryShared possibleSeparatePurposeCPU-heavy tasksScale request handlingCommunicationFastIPCTypical UsePDF generation, analyticsHTTP server scaling

Quick Decision Guide

Database Query

await db.query(...)

Use:

Promises / Async-Await

API Call

await axios.get(...)

Use:

Promises / Async-Await

Read File

fs.readFile(...)

Use:

Async I/O

Generate Large PDF

generateHugePDF()

Use:

Worker Thread

Run Python Script

spawn('python', ['script.py'])

Use:

Child Process

Scale API Across Multiple CPU Cores

Use:

Cluster

Final Rule of Thumb

  • Waiting for something external (DB, API, file, Redis) → Use Promises and Async/Await.
  • Need delayed execution → Use setTimeout().
  • Need heavy JavaScript computation → Use Worker Threads.
  • Need to run another program (Python, FFmpeg, Git) → Use Child Processes.
  • Need to scale a server across CPU cores → Use Clusters.

Understanding this distinction is one of the biggest steps toward mastering Node.js internals and performing well in backend interviews.

--

--