Phase 1 — Foundations lesson-0007

The Event Loop In Depth

The single most important concept for writing high-performance Node.js. Once you see it, you can't unsee it.

The Core Mental Model

Node.js runs on one thread. That thread runs an infinite loop — the event loop. Each "tick" of the loop processes work from several queues in a fixed priority order.

┌─────────────────┐ │ Your Code │ │ (call stack) │ └────────┬──────────┘ │ when stack is empty ▼ ┌──────────────────────────────┐ │ Event Loop │ │ │ │ 1. microtasks │ ← Promises, queueMicrotask │ (drain completely) │ │ │ │ 2. nextTick queue │ ← process.nextTick (Node-only) │ │ │ 3. I/O callbacks │ ← fs, net, http responses │ │ │ 4. timers │ ← setTimeout, setInterval │ │ │ 5. setImmediate │ ← runs after I/O in same iteration │ │ └──────────────────────────────┘

The Queues — Priority Order

Microtask queueHIGHEST
Resolved Promises (.then callbacks), async/await continuations, queueMicrotask(). Drains completely before anything else runs.
nextTick queueHIGH
process.nextTick() — Node.js specific. Runs before I/O even within the same loop phase. Use sparingly.
I/O callbacksNORMAL
Callbacks from completed async I/O — file reads, network responses, database query results.
TimersNORMAL
setTimeout and setInterval callbacks — fire when their delay has elapsed AND the loop reaches the timer phase.
setImmediateAFTER I/O
Runs after I/O callbacks in the current loop iteration. Useful for deferring work until I/O is processed.

Predict the Output

The classic interview question. Trace through the queues manually, then run it:

console.log('1 — synchronous');

setTimeout(() => console.log('2 — setTimeout(0)'), 0);

Promise.resolve().then(() => console.log('3 — Promise.then'));

process.nextTick(() => console.log('4 — process.nextTick'));

console.log('5 — synchronous');
Click Run to see the execution order with explanations...

Why setTimeout(fn, 0) Isn't Instant

setTimeout(fn, 0) does NOT run immediately. It schedules the callback to run on the next loop iteration's timer phase — only after the call stack is empty and all microtasks/nextTick have run. The minimum delay is actually ~1ms regardless of what you pass.

Blocking the Event Loop — The Cardinal Sin

While synchronous code runs, nothing else can. Slow synchronous operations freeze your entire server:

// ❌ BAD — blocks the loop while computing
app.get('/report', (req, res) => {
  let total = 0;
  for (let i = 0; i < 1_000_000_000; i++) total += i;
  // While this runs, ALL other requests are frozen
  res.json({ total });
});

// ✅ GOOD — offload CPU work to a worker thread
const { Worker } = require('worker_threads');
app.get('/report', (req, res) => {
  const worker = new Worker('./compute.js');
  worker.on('message', (total) => res.json({ total }));
  // Main thread is free to handle other requests
});

Practical Rules

  • Use async/await for all I/O — database, file system, HTTP calls. It never blocks the loop.
  • Never use synchronous versions of async functions in request handlers: no fs.readFileSync(), no JSON.parse() on huge inputs inside hot paths.
  • Offload CPU-heavy tasks to worker_threads or a separate process.
  • Profile with clinic.js if your server feels slow — it visualises event loop lag in real time.
The Practical Takeaway
In 99% of backend work, you never think about the event loop directly. You just: (1) always await async operations, (2) never run long synchronous loops in handlers. That's it. This lesson gives you the mental model to understand why those rules exist.

🧠 Check Your Understanding


Go Deeper

Primary source: Node.js docs — The Node.js Event Loop — official, authoritative.

Video: What the heck is the event loop anyway? — Philip Roberts (JSConf) — the best visual explanation ever made.

Ask your teacher: "Demonstrate event loop blocking with a real before/after benchmark." or "When would I actually use process.nextTick vs Promise.resolve?"