Understanding Node.js Concurrency: Event Loop, Promises, Worker Threads, Child Processes, and Clusters
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 ThreadThe 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
≠
ThreadPromises 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 callbackThe 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 waitsThis 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 = 0Worker 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 ThreadEach 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 ThreadShared 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 2When multiple workers write to shared memory, Atomics should be used to avoid race conditions.
Processing Large JSON Files
Suppose you have:
10 million recordsInstead 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-10MEach 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 ProcessEach 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 4Each 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 8This 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-AwaitAPI Call
await axios.get(...)Use:
Promises / Async-AwaitRead File
fs.readFile(...)Use:
Async I/OGenerate Large PDF
generateHugePDF()Use:
Worker ThreadRun Python Script
spawn('python', ['script.py'])Use:
Child ProcessScale API Across Multiple CPU Cores
Use:
ClusterFinal 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.







