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:
Timers — Executes callbacks scheduled by
setTimeout()andsetInterval(). Note: timers specify a minimum delay, not an exact delay. If the event loop is busy, the callback fires later.Pending Callbacks — Executes I/O callbacks deferred to this iteration (e.g., TCP errors).
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.
Check — Executes
setImmediate()callbacks. These always run after the poll phase completes, making them useful for running code after I/O.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:
process.nextTick()queue — Always runs first (highest priority)- 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
1// Deep dive into event loop execution order23// 1. Classic event loop ordering puzzle4console.log("1. Script start (synchronous)");56setTimeout(() => {7 console.log("2. setTimeout (timers phase)");8}, 0);910setImmediate(() => {11 console.log("3. setImmediate (check phase)");12});1314Promise.resolve().then(() => {15 console.log("4. Promise.then (microtask)");16});1718process.nextTick(() => {19 console.log("5. process.nextTick (microtask - highest priority)");20});2122console.log("6. Script end (synchronous)");2324// 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 is30// 3. setImmediate (check phase) ← non-deterministic here!3132// ---3334// 2. Inside I/O callback — setImmediate ALWAYS fires before setTimeout35const fs = require("fs");3637fs.readFile(__filename, () => {38 console.log("\n--- Inside I/O callback ---");3940 setTimeout(() => {41 console.log("setTimeout inside I/O");42 }, 0);4344 setImmediate(() => {45 console.log("setImmediate inside I/O"); // ← ALWAYS first inside I/O46 });4748 process.nextTick(() => {49 console.log("nextTick inside I/O"); // ← ALWAYS before both50 });51});5253// Output:54// nextTick inside I/O55// setImmediate inside I/O ← guaranteed before setTimeout in I/O context56// setTimeout inside I/O5758// ---5960// 3. Dangerous: process.nextTick starvation61// ❌ BAD — This starves the event loop!62// function recursiveNextTick() {63// process.nextTick(recursiveNextTick);64// // Event loop NEVER advances to I/O — everything hangs65// }66// recursiveNextTick();6768// ✅ GOOD — Use setImmediate for recursive patterns69function 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 iterations75 });76}77recursiveImmediate(1);7879// ---8081// 4. Nested microtasks — understanding queue draining82process.nextTick(() => {83 console.log("\nnextTick 1");84 process.nextTick(() => {85 console.log("nextTick 2 (nested)");86 // Nested nextTick runs BEFORE promises!87 });88});8990Promise.resolve().then(() => {91 console.log("Promise 1");92 return Promise.resolve();93}).then(() => {94 console.log("Promise 2 (chained)");95});9697// Output:98// nextTick 199// nextTick 2 (nested) ← entire nextTick queue drains first100// Promise 1101// Promise 2 (chained)
🏋️ Practice Exercise
Exercises:
- Predict the output of a script with
setTimeout,setImmediate,Promise.then, andprocess.nextTick— then run it to verify - Write a script that demonstrates
setImmediatealways fires beforesetTimeout(fn, 0)inside an I/O callback - Create a
process.nextTickstarvation scenario and fix it usingsetImmediate - Build a simple task scheduler that uses
setImmediateto yield to the event loop between CPU-heavy chunks - Use
perf_hooksto measure the time between scheduling asetTimeout(fn, 0)and its actual execution - 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 ~1msUsing
process.nextTick()recursively — this starves the event loop and prevents I/O from being processed, freezing the applicationConfusing 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.readFileSyncin a server) — this freezes ALL concurrent connectionsNot 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.