Node.js Event Loop Explained
Node.js is an open-source and cross-platform JavaScript runtime environment. It is a popular tool for almost any kind of project. it is famous for being fast and scalable, but many developers struggle to understand how it handles multiple tasks with a single thread.
The answer is the Event Loop.
In this article, we will understand:
What the Event Loop is
How Node.js handles asynchronous tasks
Event Loop phases
setTimeout()vssetImmediate()process.nextTick()Real-world project examples
What is the Node.js Event Loop?
The Event Loop is the mechanism that allows Node.js to perform non-blocking operations using a single thread.Normally, JavaScript runs one task at a time, but Node.js can handle thousands of requests simultaneously.
How? Because heavy operations like:
File reading
Network requests
Database queries
are handled by the system kernel or background threads, while Node.js continues executing other code.Once the background task finishes, the Event Loop executes the callback.
Simple Way
Request comes → Background work starts → Event loop waits
Work finishes → Callback added to queue → Event loop executes it
Imagine a restaurant waiter.
The waiter:
Takes an order
Sends it to the kitchen
Takes another order from another customer
Delivers food when ready
The waiter doesn't wait near the kitchen for one order.
Similarly, Node.js doesn't block execution while waiting for operations.
Event Loop Phases
The Event Loop runs in multiple phases.
timers
pending callbacks
idle / prepare
poll
check
close callbacks
Each phase has its own queue of callbacks.
The event loop runs these phases continuously.
┌───────────────────────────┐
│ timers │
├───────────────────────────┤
│ pending callbacks │
├───────────────────────────┤
│ idle, prepare │
├───────────────────────────┤
│ poll │
├───────────────────────────┤
│ check │
├───────────────────────────┤
│ close callbacks │
└───────────────────────────┘
1.Timers Phase
The timers phase executes callbacks scheduled using:
setTimeout()setInterval()
These functions allow developers to schedule code to run after a specified minimum delay.
The delay provided to setTimeout() or setInterval() is not guaranteed to be exact. It only specifies the minimum time before the callback can run. If the event loop is busy, the callback may run later.
setTimeout(() => {
console.log("Executed after at least 100ms");
}, 100);
In this example, Node.js schedules the callback to run after at least 100 milliseconds, but the actual execution time may be slightly longer depending on the event loop workload.
2.Pending Callbacks Phase
The pending callbacks phase executes certain I/O callbacks that were postponed to the next iteration of the event loop. These callbacks usually come from system-level operations such as:
TCP connection errors
Network failures
Some low-level system errors
These operations cannot always be handled immediately, so Node.js defers them to the next event loop cycle, and they are executed during the pending callbacks phase.
Imagine a server trying to connect to another server.
const net = require("net");
const client = net.createConnection({ port: 3000 });
client.on("error", (err) => {
console.log("Connection error:", err.code);
});
If the connection fails with an error like ECONNREFUSED, Node.js may delay handling that error and process it in the pending callbacks phase of the next event loop iteration.
3.Idle, Prepare Phase
The idle and prepare phases are internal phases used by Node.js and the libuv library.
They are not directly accessible to developers and are mainly used for internal bookkeeping and preparation work before the poll phase begins.
During these phases, Node.js may:
Prepare internal data structures
Perform internal checks
Set up the environment before polling for new I/O events
Since these phases are handled internally by Node.js, developers usually do not interact with them directly.
4.Poll Phase
The poll phase is the most important phase of the event loop. Its main responsibilities are:
Retrieve new I/O events
Execute I/O-related callbacks
These I/O operations include:
File system operations (
fs.readFile)Database responses
API responses
Network requests
const fs = require("fs");
fs.readFile("data.txt", () => {
console.log("File reading completed");
});
In this example:
Node.js sends the file reading request to the operating system.
When the file reading completes, the callback is added to the poll queue.
The event loop executes it during the poll phase.
If there are no callbacks in the poll queue, Node.js may wait (block) for new I/O events to arrive. This waiting behavior is what allows Node.js to be efficient and non-blocking.
5.Check Phase
The check phase executes callbacks scheduled using:
setImmediate() schedules a function to run immediately after the poll phase completes.
setImmediate(() => {
console.log("Executed in check phase");
});
This callback will run after the poll phase of the event loop.
6.Close Callbacks Phase
The close callbacks phase executes callbacks related to closed resources, such as sockets or handles. For example, when a socket connection closes unexpectedly, the associated callback runs in this phase.
socket.on("close", () => {
console.log("Socket connection closed");
});
This callback will run in the close callbacks phase when the socket is closed.
Understanding Async Execution in Node.js
Node.js has multiple ways to schedule asynchronous code. The most common ones are:
setTimeout()setImmediate()process.nextTick()Promise.then()queueMicrotask()
Although they look similar, they run in different queues and different phases of the Event Loop. Understanding their execution order is very important for Node.js interviews and debugging asynchronous code.
1. setTimeout() :-
setTimeout() schedules a callback to run after a minimum delay (in milliseconds). The callback is placed in the Timers phase of the Event Loop.
The delay is not exact. It only guarantees that the callback will not execute before the specified time.
Run the callback after at least 100ms . Execution may happen later if the event loop is busy.
Event Loop Phase
Timers Phase
Delayed tasks
Retry logic
Debouncing operations
Scheduling background tasks
setTimeout(() => {
console.log("Retrying API request...");
}, 2000);
2. setImmediate()
setImmediate() schedules a callback to run immediately after the poll phase finishes. The callback runs in the Check phase of the Event Loop.
setImmediate(() => {
console.log("Immediate executed");
});
Event Loop Phase
Check Phase
Running code right after I/O operations
Breaking long CPU tasks into smaller chunks
Avoiding blocking the event loop
const fs = require("fs");
fs.readFile("file.txt", () => {
setImmediate(() => {
console.log("Processing file after reading");
});
});
3. process.nextTick()
process.nextTick() schedules a callback to execute immediately after the current operation completes, before the event loop continues. It does not belong to any event loop phase. Instead, it runs in the Next Tick Queue, which has higher priority than all other queues.
console.log("Start");
process.nextTick(() => {
console.log("Next Tick");
});
console.log("End");
// output is
Start
End
Next Tick
Error handling
Ensuring asynchronous behavior
Running callbacks after the current stack finishes
Example
function validate(data, callback) {
if (!data) {
return process.nextTick(() =>
callback(new Error("Invalid data"))
);
}
}
4. Promises (Microtasks)
Promises schedule callbacks using the Microtask Queue. Promise callbacks run after the current synchronous code finishes, but before the event loop continues.
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise executed");
});
console.log("End");
// Output is
Start
End
Promise executed
Event Loop Queue
Microtask Queue
Async operations
API requests
Database queries
Modern asynchronous code using
async/await
async function getUsers() {
const users = await database.getUsers();
return users;
}
5. queueMicrotask()
queueMicrotask() schedules a function in the microtask queue, similar to Promises. It allows developers to manually schedule microtasks.
queueMicrotask(() => {
console.log("Microtask executed");
});
Event Loop Queue
Microtask Queue
Framework internals
Scheduling lightweight async tasks
Similar behavior to Promise microtasks
queueMicrotask(() => {
updateUI();
});
6. Execution Order
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
process.nextTick(() => {
console.log("nextTick");
});
Promise.resolve().then(() => {
console.log("promise");
});
queueMicrotask(() => {
console.log("queueMicrotask");
});
console.log("End");
Start
End
nextTick
promise
queueMicrotask
setTimeout / setImmediate (depends on context)
7. Execution Priority Diagram
1️⃣ Synchronous Code
↓
2️⃣ process.nextTick()
↓
3️⃣ Promise.then()
↓
4️⃣ queueMicrotask()
↓
5️⃣ Event Loop Phases
├─ Timers (setTimeout)
├─ Poll (I/O)
├─ Check (setImmediate)
└─ Close callbacks
8.Inside an I/O operation:
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});
// output is
immediate
timeout
Poll phase finishes
Check phase runs (
setImmediate)Timers phase runs later
9. Quick Comparison Table
| Method | Queue / Phase | Execution Time |
|---|---|---|
| process.nextTick | Next Tick Queue | Immediately after current operation |
| Promise.then | Microtask Queue | After nextTick but before event loop |
| queueMicrotask | Microtask Queue | Same as Promise microtasks |
| setTimeout | Timers Phase | After minimum delay |
| setImmediate | Check Phase | After poll phase |
Important facts interviewers expect:
1️⃣ process.nextTick() runs before Promises
2️⃣ Promises run before event loop phases
3️⃣ setImmediate() runs in check phase
4️⃣ setTimeout() runs in timers phase
5️⃣ setImmediate() usually runs before timeout inside I/O