Node.js Event Loop Deep Dive

What is the Node.js Event Loop?

Unlike many other environments where each request might spawn a new thread, Node.js operates primarily on a single main thread. This sounds limiting, but it achieves high concurrency and performance through an **event-driven, non-blocking I/O model**, powered by the **Event Loop**.

Think of the Event Loop as a constantly running process that orchestrates tasks. When Node.js starts, it initializes the loop, processes the input script (which might register async operations), and then enters the loop. The loop's job is to check for pending events (like completed I/O, timers) and execute their associated callback functions.

The key is non-blocking I/O. Instead of waiting for slow operations (like reading a file or making a network request), Node.js delegates these tasks to the operating system or a worker thread pool (managed by the libuv library). When the operation completes, the OS/libuv notifies Node.js by placing the corresponding callback function into a queue. The Event Loop picks up these callbacks from the queue and executes them on the main thread when it gets a chance.

Core Components

The Event Loop Phases (Detailed Order)

The Event Loop cycles through a series of phases. In each phase, it processes callbacks specific to that phase.

   ┌───────────────────────────┐
┌─>│          timers         │<-- Check for expired setTimeout/setInterval callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks │<-- I/O callbacks deferred to the next loop iteration (e.g., certain OS events)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare     │<-- Internal use only
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   Incoming:   │
│  │           poll          │<─────┤  connections, │<-- Calculate block time, process I/O callbacks, execute most Macrotasks
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check         │<-- Execute setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks    │<-- Execute socket.on('close'), etc.
   └───────────────────────────┘

*** Microtask Queue processing happens after each callback and between phases! ***
        
  1. Timers: Executes callbacks scheduled by setTimeout() and setInterval() that have reached their threshold.
  2. Pending Callbacks: Executes I/O callbacks that were deferred to the next loop iteration (e.g., certain types of TCP errors). Mostly internal.
  3. Idle, Prepare: Internal use by Node.js only.
  4. Poll: This is a major phase.
    • Calculates how long it should block and wait for I/O events.
    • Processes events in the poll queue (primarily completed I/O callbacks, like from fs.readFile).
    • If the poll queue is empty:
      • If scripts have been scheduled by setImmediate(), the loop moves to the check phase.
      • If no setImmediate callbacks exist, the loop will wait for callbacks to be added to the queue and execute them immediately.
    • If the poll queue is *not* empty, the loop iterates through its callbacks and executes them synchronously until the queue is empty or a system-dependent limit is reached.
  5. Check: Executes callbacks scheduled using setImmediate(). These run immediately after the poll phase completes.
  6. Close Callbacks: Executes close event handlers, e.g., socket.on('close', ...).

Microtask Processing: After executing *any* callback (from timers, I/O, check, close phases) or after the initial synchronous script finishes, Node.js immediately checks and executes *all* callbacks currently in the Microtask Queue (first process.nextTick, then Promises) before moving on or starting the next loop iteration. This gives microtasks higher priority and allows them to run before other queued macrotasks.

Visualize the Event Loop

Add asynchronous tasks and step through the simulation to see how the Event Loop processes them.

Event Loop Simulation
Current Phase: Idle

Call Stack

    Node APIs / libuv (Background Tasks)

      Microtask Queue (nextTick, Promises)

        Callback (Macrotask) Queue (Timers, I/O, Immediate)

          Log messages will appear here...