Understanding the Inner Workings of Node.js

Table of contents
- What is Node.js?
- Phases of Node.js Event Loop
- Understanding the execution flow with an Example
- level 1: setTimeout() Introduces Timers Phase
- Level 2: setTimeout() vs setImmediate() Introduces Timers vs Check Phase
- Level 3: fs.readFile() + setImmediate() Introduces Poll Phase (I/O)
- Level 4: Inside the fs callback → Timer vs Immediate Compare phases inside the Poll Phase callback
- Level 5: Introduces Thread Pool + Event Loop
- Level 6: Final Code Combine fs.readFile() + setTimeout + setImmediate + cryptos + Thread pool
- References

What is Node.js?
Node.js is a runtime environment that allows you to run JavaScript code outside of a web browser, typically on the server side. It uses the V8 JavaScript engine (the same one used by Google Chrome) to execute code.
A key feature of Node.js is that an app runs in a single process, not spawning a new thread for each request. Instead, Node.js uses non-blocking, asynchronous I/ O: when an I/O operation (like reading a file or querying a database) is started, Node.js does not wait idly.
It offloads the work and continues running other code, resuming the I/O callback only when the operation completes. This design allows a single Node.js server to handle thousands of concurrent connections without the memory overhead of many threads.
Phases of Node.js Event Loop
The event loop is the core mechanism that makes Node.js asynchronous. It repeatedly cycles through several phases, each with its queue of callbacks. The main phases are:
Timers: Executes callbacks scheduled by
setTimeout()
andsetInterval()
Once their threshold expires.Pending Callbacks: Executes I/O callbacks deferred to the next loop iteration (rarely used user callbacks).
Idle, Prepare: Internal phases (not typically visible to user code).
Poll: Retrieves new I/O events from the OS. Node will block here if there are no timers due and no immediate work. It executes callbacks for completed I/O (e.g. reading a file, network activity).
Check: Executes callbacks scheduled by
setImmediate()
. These run after polling.Close Callbacks: Handles “close” events (e.g.
socket.on('close')
).
Understanding the execution flow with an Example
level 1: setTimeout()
Introduces Timers Phase
console.log("Start of Script"); //1
setTimeout(() => { // 3
console.log("Timer fired");
}, 0);
console.log("End of Script"); // 2
Start of Script
End of Script
Timer fired
Async execution with setTimeout. The timer runs in the event loop after the main script finishes, that’s why the setTimeout() is already completed its time (expired) even though it is executed after the main script.
Level 2: setTimeout() vs setImmediate() Introduces Timers vs Check Phase
console.log("Start of Script"); //1
setTimeout(() => {
console.log("Timer Phase");
}, 0); //3
setImmediate(() => {
console.log("Check Phase");
}); //4
console.log("End of Script"); //2
Start of Script
End of Script
Check Phase
Timer Phase
Timer and Immediate are scheduled differently:
Timer: runs after a minimum delay, which is 1st phase of the event loop. Immediate: runs after poll phase (like I/O), which is the 3rd step of the event loop
After the start script, the main thread will initiate the setTimeout and setImmediate() to the event loop, The event loop starts the execution phase by phase
Level 3: fs.readFile() + setImmediate() Introduces Poll Phase (I/O)
const fs = require("fs"); //1
console.log("Start of Script"); //2
fs.readFile("sample.txt", "utf-8", () => {
console.log("File Read (Poll Phase)"); //4
setImmediate(() => {
console.log("Immediate inside fs callback"); //5
});
});
console.log("End of Script"); //3
Start of Script
End of Script
File Read (Poll Phase)
Immediate inside fs callback
The fs.readFile() function is asynchronous and doesn't block the execution, so Node.js continues running other code. Once the file read is complete, its callback is executed during the Poll Phase of the Event Loop. Inside this callback, a setImmediate() is scheduled, which is then executed in the next Phase.
Level 4: Inside the fs callback → Timer vs Immediate Compare phases inside the Poll Phase callback
const fs = require("fs"); //1
fs.readFile("sample.txt", "utf-8", () => { // 2 register to event loop
console.log("File Read Complete"); //3
// 5
//why?: because currently code is on I/O phase and after comes intermediate phase
setTimeout(() => {
console.log("Timer inside readFile");
}, 0);
// 4
setImmediate(() => {
console.log("Immediate inside readFile");
});
});
File Read Complete
Immediate inside readFile
Timer inside readFile
Inside this callback, both setTimeout(..., 0) and setImmediate() are scheduled. Although both are asynchronous, setImmediate() usually runs before setTimeout() in this context. That’s because setImmediate() is queued for the next Phase, which comes immediately after the Poll Phase ends, whereas setTimeout(0) is deferred to the next Timer Phase. This highlights the internal ordering of Event Loop phases during I/O operations.
Level 5: Introduces Thread Pool + Event Loop
const crypto = require("crypto"); //1
const start = Date.now(); //2
// CPu intensive task (offloads to the thread pool)
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => { // 3 register to thread pool
console.log(`Hashed in ${Date.now() - start}ms`); // 5
});
console.log("End of Script"); // 4 -- logs to console
End of Script
Hashed in 631ms
In this example, crypto.pbkdf2() performs a CPU-intensive hashing task, but it doesn’t block the main thread. Instead, it’s handed off to the libuv thread pool, which runs it in the background. Meanwhile, log() executes immediately. When the hashing completes, its callback is pushed to the Event Loop and eventually logged. This shows how Node.js efficiently handles heavy CPU work using a thread pool, keeping the main thread responsive.
Level 6: Final Code Combine fs.readFile() + setTimeout + setImmediate + cryptos + Thread pool
const fs = require("fs");
const crypto = require("crypto");
process.env.UV_THREADPOOL_SIZE = 4; //default is 4
console.log("Start of Script"); //1
setTimeout(() => console.log("setTimeout - Timer Phase"), 0); //3
setImmediate(() => console.log("setImmediate Phase")); // 4
fs.readFile("sample.txt", "utf-8", () => {
console.log("Finished reading file - Poll Phase"); //5
setTimeout(() => {
//7
console.log(" setTimeout inside readFile - Timer Phase");
}, 0);
setImmediate(() => {
//6
console.log(" setImmediate inside readFile - Check Phase");
});
const start = Date.now();
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => {
console.log(`Password 1 hashed in ${Date.now() - start}ms`);
});
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => {
console.log(`Password 2 hashed in ${Date.now() - start}ms`);
});
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => {
console.log(`Password 3 hashed in ${Date.now() - start}ms`);
});
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => {
console.log(`Password 4 hashed in ${Date.now() - start}ms`);
});
// 1st 4 hasing will run simultaneously becase we have 4 thread pool
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => {
console.log(`Password 5 hashed in ${Date.now() - start}ms`);
});
crypto.pbkdf2("password", "salt", 100000, 512, "sha512", () => {
console.log(`Password 6 hashed in ${Date.now() - start}ms`);
});
// after complition of 4 remaining both hash function will execute simultaneously
// difference in the time proves that
});
console.log("End of Script"); //2
Start of Script
End of Script
setTimeout - Timer Phase
setImmediate Phase
Finished reading file - Poll Phase
setImmediate inside readFile - Check Phase
setTimeout inside readFile - Timer Phase
Password 2 hashed in 1011ms
Password 3 hashed in 1012ms
Password 1 hashed in 1020ms
Password 4 hashed in 1053ms
Password 6 hashed in 1735ms
Password 5 hashed in 1745ms
This shows six individual pbkdf2 calls. Each one is sent to the libuv thread pool for processing. This helps visualize how multiple expensive tasks can run in parallel without blocking the main thread. Once fs.readFile completes in the Poll Phase, it triggers the callbacks and schedules setImmediate() and all six hash operations. When each hashing completes, its callback logs the result. This makes the Event Loop behaviour and thread pool usage very easy to understand step-by-step.
References
Node.js — The Node.js Event Loop
How NodeJS Works? - You don't Know NodeJS
(122) JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue - YouTube
Subscribe to my newsletter
Read articles from JAY PATIL directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
