The Event Loop is the mechanism that allows Node.js (single-threaded) to handle multiple operations concurrently without blocking.
It continuously checks for tasks and executes them in a specific order using queues and phases.
Execution priority:
process.nextTick- Promises (
.then) - Then event loop phases
More precise
- Current code execution
- process.nextTick queue (Not part of Event Loop)
- Promise (microtask queue, Part of Event Loop, executes between each phase of event loop)
- Event loop phases (timers → poll → check → ...)
Why We need Event Loop:
Node.js runs on one main thread, so it cannot do multiple things at the exact same time.
Instead:
- It offloads heavy work (I/O, network, DB) to OS / libuv
-
Uses the event loop to process completed tasks
Node.js delegates asynchronous operations to the OS or libuv’s thread pool. Once those operations complete, their callbacks are queued, and the event loop processes and executes them in different phases. This architecture allows Node.js to handle asynchronous operations efficiently despite being single-threaded.
In Layman terms Basically Node outsources operations to libuv thread pool/OS so they run asynchronously and Event Loop is the place where calls back of those operations handled when they completed or failed, So with the help of Event loops and libuv thread pool//OS Node performs asynchronous operation.
Event Loop Phases:
- Timers
- Pending Callbacks
- Poll (I/O)
- Check
- Close Callbacks
1. Timers: Responsible to execute callbacks of timers event like setTimeout(), setInterval().
Timers phase does NOT “execute timers directly when scheduled.”
It executes callbacks of timers whose delay has already expired.
Example
Step1: When you call
setTimeout(() => console.log("hi"), 1000);
Node.js does NOT execute it immediately
Instead:
-
Registers the timer with libuv (timer system)
- Keeps track of when it should expire
Step2:
After ~1000ms:
-
Timer becomes “ready”
-
Its callback is placed in the Timers queue
Step3:
Event loop enters Timers phase
-
It checks: “Which timers have expired?”
- Executes their callbacks
setTimeout called
↓
Timer registered (libuv)
↓
Time passes
↓
Timer expires → added to timers queue
↓
Event loop enters Timers phase
↓
Callback executed
Why setTimeout(fn, 0) is NOT immediate?
setTimeout(fn, 0) does not execute immediately because the callback is scheduled and only runs when the event loop reaches the timers phase after the current execution completes.
setTimeout(() => console.log("A"), 0);
console.log("B");
Output
B
A
Main Thread sends setTimeout(() => console.log("A"), 0); to run with libuv thread pool
Main tread executes console.log("B");
0 milliseconds passed so setTimeout get registered with Timer phase
Now it get executed when execution enters into Event Loop after completing current loop.(As Timers is first stage of Event Loop)
setTimeout and setInterval register timers with the system. Once their delay expires, their callbacks are queued and executed during the Timers phase of the event loop, not immediately.
2. Pending Callbacks:
Pending Callbacks phase handles deferred system-level callbacks from the previous loop,
while Poll phase handles new I/O events and executes most I/O callbacks.
Executes callbacks that were deferred from the previous event loop iteration
Typical cases:- Some TCP errors (like
ECONNREFUSED) - System-level I/O errors
- Low-level libuv deferred callbacks
These are not your regular fs.readFile or DB call or HTTP callbacks.
Something happened earlier, but couldn’t be processed immediately → now handled here
Important:
-
Rarely used directly in application code
- Mostly internal to Node.js
In practice, developers mostly interact with the poll phase, since that's where application-level I/O happens. The pending callbacks phase is more of an internal mechanism for handling edge-case system callbacks.
3. Poll Phase: Most important phase of Event Loop
This is the main I/O execution phase.
Mostly all developer written code execute (or handled callbacks) from here.
Poll Phase = Heart of Node.js
Handles most I/O (API calls, DB, file system)
Handle incoming I/O and keep the app running.
Responsibilities:
- Execute I/O callbacks:
- File system (fs.readFile)
- Network (HTTP, DB calls)
- Wait for new I/O events if none are ready
Example
-
You call
fs.readFile() or any DB call or any API call - OS processes it
- Result comes back
Callback runs in Poll phase
4. Check Phase
Runs after Poll phase
Executes: setImmediate()
5. Close Callbacks
The Close Callbacks phase executes callbacks related to resource cleanup, especially when a handle (like a socket or stream) is closed.
When something like:
-
a socket
- a stream
- a connection
gets closed or destroyed, its cleanup callback runs here.
Executes: socket.on('close'), cleanup tasks