Node.js Internals: Event Loop & Thread Pool Explained
Understanding how Node.js works under the hood is key to writing efficient and scalable backend applications. Let’s break down the Event Loop and Thread Pool in a simple, structured way.
1. The Core Idea
JavaScript in Node.js runs on a single main thread. Yet, it can handle thousands of operations concurrently.
How?
👉 Thanks to libUV, which provides:
✅ Event Loop
✅ Thread Pool
2. How a Node.js Program Runs
When you execute a Node.js file, the flow looks like this:
The top-level code executes first, and only after that, Node.js starts handling async tasks using the event loop.
3. Event Loop
At its core, the event loop is just a loop:
while (true) {
1. Timers Phase
2. I/O Polling Phase
3. setImmediate Phase
4. Close Callbacks Phase
}
👉 It keeps running as long as there are pending tasks.
4. Thread Pool Explained
The Thread Pool is a group of background worker threads.
🔹 It handles "CPU Intensive Operations":
File system operations (
fs)Cryptography
DNS lookups
Compression / Encryption
🔹 Default size:
4 threads
🔹 You can increase it:
process.env.UV_THREADPOOL_SIZE = 8;
👉 Important: The thread pool is mainly used for CPU-intensive or blocking tasks.
5. Event Loop Phases (Detailed)
🟡 1. Timers Phase
Executes:
setTimeout()
setInterval()
Example:
setTimeout(() => console.log("Timer"), 0);
👉 Runs only after the timer expires, not immediately.
🟢 2. I/O Polling Phase (Most Important)
Executes:
- I/O callbacks (like file reading)
Example:
fs.readFile('file.txt', () => {
console.log("File Reading Completed");
});
👉 Responsibilities:
Execute completed I/O tasks
Decide what phase comes next
🟣 3. setImmediate Phase
Executes:
setImmediate(() => console.log("Immediate"));
👉 Runs right after the I/O polling phase
🔴 4. Close Callbacks Phase
Executes cleanup logic:
socket.on('close', () => {});
6. setTimeout(0) vs setImmediate()
These two often confuse developers.
| Function | When it Runs |
|---|---|
| setTimeout(fn, 0) | Next Timers phase |
| setImmediate(fn) | After I/O Polling phase finishes |
👉 setTimeout(fn, 0) is not truly immediate
7. Behavior Inside I/O
fs.readFile('file.txt', () => {
setTimeout(() => console.log("Timer"), 0);
setImmediate(() => console.log("Immediate"));
});
✅ Output:
Immediate
Timer
Why?
I/O completes → goes to setImmediate phase
Timer runs in the next loop iteration
8. Behavior Outside I/O (Unpredictable)
setTimeout(() => console.log("Timer"), 0);
setImmediate(() => console.log("Immediate"));
❓ Output:
Not fixed
👉 Depends on:
System performance
Execution timing
Event loop start timing
9. Top-Level Code Matters
setTimeout(() => console.log("Timer"), 0);
setImmediate(() => console.log("Immediate"));
console.log("Top Level");
Output:
Top Level
Timer
Immediate
👉 Because:
Top-level code delays event loop start
Timer gets time to expire first
Without Top-Level Code:
setTimeout(() => console.log("Timer"), 0);
setImmediate(() => console.log("Immediate"));
👉 Event loop starts immediately 👉 Timer may not be ready yet
Possible Output:
Immediate
Timer
10. process.nextTick()
This is not part of the event loop.
Definition:
👉 Executes right after current operation, before event loop starts.
console.log("Start");
process.nextTick(() => {
console.log("Next Tick");
});
console.log("End");
Output:
Start
End
Next Tick
11. Final Execution Priority
1. Top-level code
2. process.nextTick()
3. Event Loop starts:
→ Timers
→ I/O Polling
→ setImmediate
→ Close Callbacks
Final Thoughts
Node.js is single-threaded, but not limited
The event loop enables non-blocking execution
The thread pool handles heavy tasks in the background
Understanding phase order is critical for debugging async behavior
