Node.js Internals Explained in Simple Words

π§ What is Node.js?
Node.js is a JavaScript runtime environment that allows JavaScript to run outside the browser.
But internally, Node.js is not just JavaScript.
It is built using:
V8 Engine + libuv + C++ bindings
V8 Engine β Executes JavaScript code (turns JS into machine code)
libuv β Handles asynchronous operations (event loop + thread pool)
C++ bindings β Bridge between JavaScript and system (OS)
ποΈ Core Architecture of Node.js
1. Process (What happens when Node starts)
When you run a Node.js file, a process is created.
A process means:
A running instance of a program with memory, resources, and execution flow.
2. Main Thread (Very Important Concept)
Node.js runs on a single main thread.
This main thread is responsible for:
executing JavaScript code
handling callbacks
managing the event loop
delegating heavy tasks
π BUT it does NOT handle heavy work directly.
Instead, it delegates them to libuv thread pool or OS system APIs.
π What happens inside the Main Thread?
When Node starts execution, it follows these steps:
A. Project Initialization Phase
Node loads your project:
reads entry file (index.js / app.js)
initializes memory
prepares runtime environment
B. Top-Level Code Execution
All synchronous code runs first:
variable declarations
console logs
function definitions
π This is the first execution phase
C. Import / Module Loading Phase
When you write:
import fs from "fs";
Node does this:
What happens internally:
Resolves module path
Loads built-in modules (fs, crypto, path, etc.)
Wraps your file inside a function scope
Executes module code BEFORE your main logic continues
π Thatβs why imports are NOT random β they are resolved first.
D. Callback Registration Phase
At this stage:
Node sees async functions like:
setTimeout
setImmediate
fs.readFile
Promises
π It does NOT execute them immediately.
Instead, it:
Registers them in the background system (event loop queues / thread pool / OS handlers)
E. Event Loop Starts
After top-level code is done:
π libuv starts the event loop
Now Node continuously checks:
timers
I/O operations
callbacks
thread pool results
π Event Loop Phases
1. Timers phase(expired callbacks):
This phase runs:
setTimeoutsetInterval
Why called βexpired callbacksβ?
Because:
Only timers whose delay has finished are executed here.
If delay = 0 β it becomes eligible immediately, but still depends on event loop timing.
2. I/O Polling Phase:
I/O polling is the phase of the event loop where Node.js interacts with the background system to handle asynchronous operations. When tasks like file reading, crypto hashing, or DNS lookups are triggered, Node.js first delegates them to the libuv thread pool or OS, so the main thread doesnβt get blocked. During the polling phase, the event loop continuously checks whether these background tasks are completed, collects their results once they finish, and prepares their callbacks to be executed in the next appropriate phase of the event loop.
π Event loop does NOT do heavy work itself.
Instead:
libuv thread pool does the work
event loop just waits + checks results
That waiting + checking is called:
I/O Polling
3. setImmediate() Phase :
The setImmediate() phase is the part of the event loop where Node.js executes all callbacks scheduled using setImmediate(). After the I/O polling phase finishes collecting results from background tasks, the event loop moves into this stage and runs these callbacks right away in the same iteration. This phase ensures that tasks scheduled with setImmediate() are executed immediately after I/O operations are completed, but before the event loop proceeds to handle close callbacks or begins the next loop cycle.
setImmediate() is a Node.js function used to schedule a callback to run after the I/O polling phase of the event loop completes. It does not depend on time like setTimeout; instead, it depends on the event loop cycle. When setImmediate() is called, its callback is placed into the check queue, and it is executed in the same event loop iteration right after I/O tasks are finished.
4. Close callback() phase :
The Close Callback phase is the part of the event loop where Node.js executes callbacks that are triggered when certain resources are closed or cleaned up. This includes events like closing a socket, ending a file stream, or shutting down a connection. After all other phases like timers, I/O polling, and setImmediate() are completed, Node.js enters this phase to safely handle cleanup-related tasks so that resources are properly released before the event loop moves to the next iteration or exits if nothing is left to process.
example :
socket.on("close", callback)
or
process.on("SIGINT", () => {
console.log("Ctrl + C pressed (SIGINT received)");
console.log("Cleaning up before exit...");
process.exit();
});
console.log("Running... Press Ctrl + C");
5. Check for pending tasks :
After each cycle:
Node checks:
Is anything pending?
Any timer left?
Any I/O left?
π If YES β next loop iteration
π If NO β process exits
After each event loop cycle, Node.js checks whether there is any pending work such as active timers, ongoing I/O operations, or open handles like servers and streams. If any of these exist, the event loop continues to the next iteration. If nothing is left, Node.js safely exits the process. setImmediate() is not directly part of this exit check; it is simply executed during the event loop cycle when reached.
π§ͺ Example 1 (Basic Event Loop Understanding)
import fs from "fs";
setTimeout(() => {
console.log("inside setTimeout");
}, 0);
setImmediate(() => {
console.log("inside setImmediate 1");
});
fs.readFile("sample.txt", "utf-8", function (err, data) {
console.log("file read success");
setTimeout(() => {
console.log("inside setTimeout 2");
}, 0);
setTimeout(() => {
console.log("inside setTimeout 3");
}, 0);
setImmediate(() => {
console.log("inside setImmediate 2");
});
});
console.log("top level code");
π§ Execution Explanation (Step by Step)
Step 1: Top-level code runs first
Step 2: Timers & Immediate are registered
They go into event loop queues:
setTimeout β timer queue
setImmediate β check queue
Step 3: File reading starts
libuv thread pool handles it
main thread continues
Step 4: Event loop begins
Order depends on phases:
Typical output:
top level code
inside setTimeout
inside setImmediate 1
file read success
inside setImmediate 2
inside setTimeout 2
inside setTimeout 3
π BUT exact order can vary slightly depending on system timing.
β‘ Example 2 (CPU Intensive + Thread Pool Deep Understanding)
import fs from "fs";
import crypto from "crypto";
const start = Date.now();
// Thread pool size
process.env.UV_THREADPOOL_SIZE = 4;
setTimeout(() => {
console.log("Hello from Timer");
}, 0);
setImmediate(() => {
console.log("Hello from Immediate");
});
fs.readFile("sample.txt", "utf-8", function (err, data) {
console.log("File Reading Complete");
setTimeout(() => console.log("Time 2"), 0);
setTimeout(() => console.log("Time 3"), 0);
setImmediate(() => console.log("Immediate 2"));
// CPU intensive tasks (thread pool)
crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
console.log("Password 1 hashed", Date.now() - start);
});
crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
console.log("Password 2 hashed", Date.now() - start);
});
crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
console.log("Password 3 hashed", Date.now() - start);
});
crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
console.log("Password 4 hashed", Date.now() - start);
});
crypto.pbkdf2("password", "salt", 300000, 1024, "sha256", () => {
console.log("Password 5 hashed", Date.now() - start);
});
});
console.log("Hello from Top Level Code");
π§ What happens internally?
- First 4 crypto tasks run in thread pool
Because default pool size = 4
- 5th crypto task waits
Because no thread is free
- As soon as one finishes
5th task starts
β‘ Key Insight
π Thread pool is like a small worker team π If workers are busy β tasks wait in queue
π¦ Queues in Node.js Event Loop
Node.js event loop has several important queues where different types of callbacks are stored before execution:
1. β±οΈ Timer Queue:
Stores callbacks from setTimeout() and setInterval()
Runs when the timer duration has expired
2. π‘ I/O Queue (Poll Queue)
Stores callbacks from I/O operations like: file reading (fs) network requests database operations
These are executed after background tasks are completed
3.β‘ Check Queue
Stores callbacks from setImmediate()
Runs immediately after the I/O polling phase
4. π Close Queue
Stores callbacks for cleanup events
Example: socket close events stream close events
5. π Microtask Queue (Higher Priority)
- Runs between phases:
- process.nextTick() (highest priority)
- Promises (.then, .catch)
π Microtasks run before moving to the next queue
Note: Different internal systems (like libuvβs timer system, thread pool, and OS) are responsible for pushing callbacks into their respective queues when they become ready, while the event loopβs job is simply to process and execute those callbacks phase by phase.
π§ Who puts expired callbacks into queues?
Node.js internal systems (libuv + timers system + OS) put callbacks into queues β NOT the event loop itself.
1. Timers (setTimeout, setInterval)
When you write:
setTimeout(fn, 1000);
What happens:
Node registers the timer and starts counting time
Internally, libuv maintains a timer system (like a timer list/heap)
π When time expires:
libuv moves the callback into the timer queue
β Event loop did NOT do this
β libuv/timer system did it
2. I/O operations (fs, network, crypto)
When you write:
fs.readFile("file.txt", fn);
What happens:
Task goes to thread pool or OS
Work happens in background
π When task finishes:
OS / libuv pushes the callback into the I/O queue
3. setImmediate()
setImmediate(fn);
π This is simpler:
Callback is directly placed into the check queue during registration
libuv stores the callback in the check queue
(No waiting, no background work)
4. Close callbacks
socket.on("close", fn);
π When resource closes:
Node pushes callback into close queue



