Node.js Event Loop
HardNode.js uses libuv to implement its event loop, which has 6 distinct phases. Understanding these phases helps you predict execution order and avoid blocking the server.
Interactive Visualization
Codesync
1setTimeout(() => {2 console.log('timeout');3}, 0);45setImmediate(() => {6 console.log('immediate');7});89console.log('sync');
process.nextTick
(empty)
Promise Queue
(empty)
Event Loop Phases
Timers
-
Pending
-
Poll
-
Check
-
Close
-
Script starts. Node.js event loop initializes.
1 / 6
Use ← → keys to navigate, R to reset
Key Insight: In main module, setTimeout vs setImmediate order varies! Inside I/O callbacks, setImmediate always runs first.
Key Points
- Node.js event loop has 6 phases (not just micro/macro queues)
- Timers phase: executes setTimeout/setInterval callbacks
- Poll phase: retrieves new I/O events, executes I/O callbacks
- Check phase: executes setImmediate callbacks
- process.nextTick runs BETWEEN phases (highest priority)
- Blocking the event loop freezes your entire server
Code Examples
Event Loop Phases
// Phase 1: Timers (setTimeout) setTimeout(() => console.log("timer"), 0); // Phase 4: Poll (I/O callbacks) fs.readFile("file.txt", () => { console.log("file read"); }); // Phase 5: Check (setImmediate) setImmediate(() => console.log("immediate")); // Between phases: process.nextTick process.nextTick(() => console.log("nextTick")); // Output order depends on I/O!
Different callbacks run in different phases
setTimeout vs setImmediate
// In main module: order varies! setTimeout(() => console.log("timeout"), 0); setImmediate(() => console.log("immediate")); // Inside I/O callback: immediate first! fs.readFile("file.txt", () => { setTimeout(() => console.log("timeout"), 0); setImmediate(() => console.log("immediate")); }); // Output: immediate, timeout // (Check phase runs before Timers)
setImmediate runs before setTimeout in I/O callbacks
process.nextTick
// nextTick runs BETWEEN phases // (even before Promises!) Promise.resolve().then(() => { console.log("promise"); }); process.nextTick(() => { console.log("nextTick"); }); // Output: nextTick, promise // nextTick has highest priority!
process.nextTick runs before Promise callbacks
nextTick Starvation
// DANGER: Recursive nextTick // blocks the event loop forever! function recurse() { process.nextTick(recurse); } recurse(); // I/O callbacks will NEVER run // Server becomes unresponsive! // Fix: use setImmediate instead function safeRecurse() { setImmediate(safeRecurse); }
nextTick can starve I/O if abused
Blocking the Event Loop
// BAD: Blocks entire server! app.get("/slow", (req, res) => { // 10 billion iterations for (let i = 0; i < 1e10; i++) {} res.send("done"); }); // GOOD: Use Worker Threads const { Worker } = require("worker_threads"); app.get("/fast", (req, res) => { const worker = new Worker("./heavy.js"); worker.on("message", () => res.send("done")); });
CPU-heavy work blocks all requests
Event Loop Monitoring
// Monitor event loop lag const start = process.hrtime(); setImmediate(() => { const [s, ns] = process.hrtime(start); const lag = s * 1000 + ns / 1e6; console.log(`Event loop lag: ${lag}ms`); }); // High lag = event loop blocked // Target: < 100ms for responsive server
Monitor lag to detect blocking issues
Common Mistakes
- Confusing browser event loop with Node.js event loop
- Using recursive process.nextTick (causes starvation)
- Blocking the event loop with CPU-intensive code
- Assuming setTimeout(fn, 0) runs immediately
Interview Tips
- Know all 6 phases of the libuv event loop
- Explain the difference between setImmediate and setTimeout
- Understand why process.nextTick exists and when to use it
- Know how to handle CPU-intensive tasks (Worker Threads)