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
