Node.js Event Loop

The event loop is the heart of Node.js. It decides which task runs and when. Understanding it explains why Node.js handles many users with a single main thread.

A Simple Picture

Imagine a chef in a kitchen with a pile of order slips. The chef takes one slip, starts the dish, and moves to the next slip. A timer rings when the oven finishes. The chef plates that dish and continues. The chef never stands idle beside the oven.

 Order slips (tasks)  --->  [ CHEF = Event Loop ]
                                  |
                  starts slow jobs in the background
                                  |
              oven rings (job done) --> plate the dish (callback)

Main Parts

Call Stack

The call stack holds the function that runs right now. Node.js runs one function at a time. A function leaves the stack when it ends.

Background Workers

Slow jobs such as file reads and network calls run outside the stack. The system or the thread pool handles them.

Callback Queue

A finished job places its callback in a queue. The callback waits there for its turn.

Event Loop

The loop watches the call stack. An empty stack signals the loop to move the next callback from the queue onto the stack.

 +-------------+      +---------------------+
 | Call Stack  |      |  Background Workers |
 | (runs code) |----->|  (files, network)   |
 +-------------+      +---------------------+
       ^                         |
       |                         v
       |                +-----------------+
       +----------------| Callback Queue  |
         Event Loop     +-----------------+
         moves callbacks when the stack is empty

Watch It Work

console.log("1: Start");

setTimeout(() => {
  console.log("3: Timer finished");
}, 0);

console.log("2: End");

The output shows 1, 2, and 3. The timer waits in the queue even with a zero delay. The main code finishes before the loop picks the callback.

Phases of the Loop

The loop runs through phases in a fixed circle. Each phase handles one kind of callback.

   +--> 1. Timers          (setTimeout, setInterval)
   |    2. Pending         (system errors)
   |    3. Poll            (new input, file and network results)
   |    4. Check           (setImmediate)
   |    5. Close           (closed connections)
   +----------- repeat ---------+
  • Timers run callbacks whose delay has passed.
  • Poll collects finished input and output work.
  • Check runs setImmediate callbacks.
  • Close handles cleanup events.

Microtasks Jump the Queue

Promises and process.nextTick create microtasks. Microtasks run immediately after the current function, before the loop moves to another phase.

console.log("A");

setTimeout(() => console.log("B: timeout"), 0);
setImmediate(() => console.log("C: immediate"));
Promise.resolve().then(() => console.log("D: promise"));
process.nextTick(() => console.log("E: nextTick"));

console.log("F");

The typical order is A, F, E, D, B, C. The two timer styles B and C can swap places in the main file. The microtasks E and D always come first.

Priority (high to low):
  1. Current code
  2. process.nextTick
  3. Promise callbacks
  4. Timers
  5. setImmediate

Blocking the Loop

A long task on the call stack freezes the loop. Every user waits until the task ends.

// Bad: blocks all users for several seconds
function slowTask() {
  let total = 0;
  for (let i = 0; i < 5e9; i++) total += i;
  return total;
}

Ways to Avoid Blocking

  • Use asynchronous functions for files and networks.
  • Split big loops into smaller pieces with setImmediate.
  • Move heavy math to worker threads.
  • Avoid the Sync versions of functions in servers, such as readFileSync.

Why This Design Works

Most web apps spend time waiting for databases and files. The event loop uses that waiting time to serve other users. One thread handles thousands of connections with little memory.

Practice Tasks

  • Predict the output order of a script that mixes setTimeout, a promise, and a plain log, then run it to check.
  • Write a loop that blocks the program for two seconds and watch a timer arrive late.
  • Replace the blocking loop with chunks that call setImmediate.