♻️

Node.js Event Loop

advanced

Node.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);
4
5setImmediate(() => {
6 console.log('immediate');
7});
8
9console.log('sync');
process.nextTick
(empty)
Promise Queue
(empty)
Timers
-
Pending
-
Poll
-
Check
-
Close
-
Step 1/6Script starts. Node.js event loop initializes.
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)

🔗Related Concepts