The Event Loop — Deep Dive

📖 Concept

The event loop is the heart of Node.js. It's what makes non-blocking I/O possible and understanding it is the single most important concept for writing performant Node.js applications.

The event loop phases (in order):

   ┌───────────────────────────┐
┌─>│        timers              │  ← setTimeout, setInterval callbacks
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │     pending callbacks      │  ← I/O callbacks deferred from previous cycle
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │       idle, prepare        │  ← Internal use only
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │          poll              │  ← Retrieve new I/O events; execute I/O callbacks
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │          check             │  ← setImmediate callbacks
│  └──────────┬────────────────┘
│  ┌──────────┴────────────────┐
│  │     close callbacks        │  ← socket.on('close', ...) etc.
│  └──────────┘────────────────┘

Key phases explained:

  1. Timers — Executes callbacks scheduled by setTimeout() and setInterval(). Note: timers specify a minimum delay, not an exact delay. If the event loop is busy, the callback fires later.

  2. Pending Callbacks — Executes I/O callbacks deferred to this iteration (e.g., TCP errors).

  3. Poll — The most important phase. It retrieves new I/O events and executes their callbacks. If no timers are scheduled and no I/O is pending, the event loop blocks here, waiting for new events.

  4. Check — Executes setImmediate() callbacks. These always run after the poll phase completes, making them useful for running code after I/O.

  5. Close Callbacks — Executes close event callbacks (e.g., socket.on('close')).

Microtask queues (between every phase): Between each phase, Node.js processes two microtask queues:

  1. process.nextTick() queue — Always runs first (highest priority)
  2. Promise microtask queue — .then(), .catch(), .finally() callbacks
Phase → nextTick queue → Promise queue → Next Phase → nextTick queue → Promise queue → ...

⚠️ Critical: process.nextTick() can starve the event loop if called recursively — it runs before ANY I/O, so infinite nextTick calls prevent the event loop from advancing.

setTimeout(fn, 0) vs setImmediate(fn):

  • In the main module, the order is non-deterministic (depends on process performance)
  • Inside an I/O callback, setImmediate() always fires first (check phase comes right after poll)

🏠 Real-world analogy: The event loop is like a restaurant waiter serving tables. They don't stand at one table waiting for food — they take orders (register callbacks), check the kitchen (poll for I/O), deliver food when ready (execute callbacks), and handle bills (timers). One waiter can efficiently serve many tables because most time is spent waiting for the kitchen.

💻 Code Example

codeTap to expand ⛶
1// Deep dive into event loop execution order
2
3// 1. Classic event loop ordering puzzle
4console.log("1. Script start (synchronous)");
5
6setTimeout(() => {
7 console.log("2. setTimeout (timers phase)");
8}, 0);
9
10setImmediate(() => {
11 console.log("3. setImmediate (check phase)");
12});
13
14Promise.resolve().then(() => {
15 console.log("4. Promise.then (microtask)");
16});
17
18process.nextTick(() => {
19 console.log("5. process.nextTick (microtask - highest priority)");
20});
21
22console.log("6. Script end (synchronous)");
23
24// Output ORDER:
25// 1. Script start (synchronous)
26// 6. Script end (synchronous)
27// 5. process.nextTick (microtask - highest priority)
28// 4. Promise.then (microtask)
29// 2. setTimeout (timers phase) ← order of 2 & 3 is
30// 3. setImmediate (check phase) ← non-deterministic here!
31
32// ---
33
34// 2. Inside I/O callback — setImmediate ALWAYS fires before setTimeout
35const fs = require("fs");
36
37fs.readFile(__filename, () => {
38 console.log("\n--- Inside I/O callback ---");
39
40 setTimeout(() => {
41 console.log("setTimeout inside I/O");
42 }, 0);
43
44 setImmediate(() => {
45 console.log("setImmediate inside I/O"); // ← ALWAYS first inside I/O
46 });
47
48 process.nextTick(() => {
49 console.log("nextTick inside I/O"); // ← ALWAYS before both
50 });
51});
52
53// Output:
54// nextTick inside I/O
55// setImmediate inside I/O ← guaranteed before setTimeout in I/O context
56// setTimeout inside I/O
57
58// ---
59
60// 3. Dangerous: process.nextTick starvation
61// ❌ BAD — This starves the event loop!
62// function recursiveNextTick() {
63// process.nextTick(recursiveNextTick);
64// // Event loop NEVER advances to I/O — everything hangs
65// }
66// recursiveNextTick();
67
68// ✅ GOOD — Use setImmediate for recursive patterns
69function recursiveImmediate(count) {
70 if (count > 5) return;
71 setImmediate(() => {
72 console.log(`setImmediate iteration ${count}`);
73 recursiveImmediate(count + 1);
74 // Event loop CAN process I/O between iterations
75 });
76}
77recursiveImmediate(1);
78
79// ---
80
81// 4. Nested microtasks — understanding queue draining
82process.nextTick(() => {
83 console.log("\nnextTick 1");
84 process.nextTick(() => {
85 console.log("nextTick 2 (nested)");
86 // Nested nextTick runs BEFORE promises!
87 });
88});
89
90Promise.resolve().then(() => {
91 console.log("Promise 1");
92 return Promise.resolve();
93}).then(() => {
94 console.log("Promise 2 (chained)");
95});
96
97// Output:
98// nextTick 1
99// nextTick 2 (nested) ← entire nextTick queue drains first
100// Promise 1
101// Promise 2 (chained)

🏋️ Practice Exercise

Exercises:

  1. Predict the output of a script with setTimeout, setImmediate, Promise.then, and process.nextTick — then run it to verify
  2. Write a script that demonstrates setImmediate always fires before setTimeout(fn, 0) inside an I/O callback
  3. Create a process.nextTick starvation scenario and fix it using setImmediate
  4. Build a simple task scheduler that uses setImmediate to yield to the event loop between CPU-heavy chunks
  5. Use perf_hooks to measure the time between scheduling a setTimeout(fn, 0) and its actual execution
  6. Write a script that logs which event loop phase each callback runs in by interleaving timers, I/O, and immediates

⚠️ Common Mistakes

  • Assuming setTimeout(fn, 0) fires instantly — it schedules for the NEXT iteration's timers phase, with a minimum delay of ~1ms

  • Using process.nextTick() recursively — this starves the event loop and prevents I/O from being processed, freezing the application

  • Confusing the event loop with JavaScript's call stack — the event loop manages WHEN callbacks run; the call stack manages HOW functions execute

  • Blocking the event loop with synchronous operations (e.g., fs.readFileSync in a server) — this freezes ALL concurrent connections

  • Not understanding that Promise microtasks run before I/O callbacks — a chain of 1000 .then() calls blocks I/O processing

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for The Event Loop — Deep Dive. Login to unlock this feature.