Understanding Node.js Event Loop Phases

The Node.js event loop is the core mechanism that enables asynchronous, non-blocking I/O operations despite JavaScript being single-threaded. Built on top of the Libuv library, the event loop continuously orchestrates the execution of callbacks, network events, and system operations across structured stages. This article breaks down the distinct phases of the Node.js event loop in the exact order they execute during backend execution, explaining what happens at each step and how microtasks interact with the lifecycle.


1. Timers Phase

The event loop cycle begins in the Timers phase. This phase executes callbacks scheduled by setTimeout() and setInterval().

The event loop checks if any timers have reached their specified threshold duration. If a timer has expired, its callback is invoked. Timers only specify the minimum threshold after which a callback can be executed, not the exact execution time, as subsequent phases and system load may introduce slight delays.

2. Pending Callbacks (I/O Callbacks) Phase

This phase executes system-level callbacks that were deferred from the previous loop iteration.

These typically involve low-level operations such as certain operating system reports, TCP errors (for instance, receiving an ECONNREFUSED error when attempting to connect a socket), or pending stream operations that could not be processed immediately during the poll phase.

3. Idle, Prepare Phase

The Idle and Prepare phase is strictly used internally by Node.js and Libuv.

Developers cannot directly schedule code to run in this phase. Node.js uses it to gather internal system state and prepare resources for the upcoming polling operations.

4. Poll Phase

The Poll phase is one of the most critical stages of the event loop. It serves two primary functions: 1. Calculating how long it should block and wait for new I/O events. 2. Processing events in the poll queue (such as incoming HTTP requests, database responses, and file system reads).

When the event loop enters the poll phase: * If the poll queue is not empty: The event loop synchronously executes the callbacks in the queue until the queue is exhausted or the system execution limit is reached. * If the poll queue is empty: * If scripts have been scheduled with setImmediate(), the event loop ends the poll phase and proceeds to the Check phase. * If no setImmediate() scripts exist, the event loop waits for new I/O callbacks to be added to the queue and executes them immediately. However, if any timers have expired while waiting, the loop wraps around to the Timers phase.

5. Check Phase

The Check phase allows developers to execute callbacks immediately after the poll phase completes.

This phase is dedicated exclusively to callbacks registered using setImmediate(). If the poll phase becomes idle or detects that setImmediate() callbacks are queued, it advances to this phase to prevent I/O starvation.

6. Close Callbacks Phase

The final phase of the loop handles the cleanup of resources.

If a socket or handle is closed abruptly (e.g., calling socket.destroy()) or emits a close event (e.g., readableStream.on('close', ...)), its corresponding cleanup callback is executed during this phase.


Microtasks: process.nextTick() and Promises

Microtasks are not technically part of the Libuv event loop phases, but they heavily influence execution order. Microtask queues run immediately after the current operation finishes, before the event loop transitions to the next phase.

Microtasks are processed in two separate priority queues: 1. process.nextTick() Queue: Has the highest priority and drains completely before other microtasks. 2. Promise Microtask Queue: Executes resolved Promise callbacks (.then(), .catch(), .finally(), and async/await continuations) directly after the nextTick queue empties.