Lab 02: Event Loop Advanced

Time: 60 minutes | Level: Architect | Docker: docker run -it --rm node:20-alpine sh

The Node.js event loop is the heartbeat of every server. This lab dissects libuv's phase architecture, microtask queue ordering, async context propagation with async_hooks, and blocking event loop detection.


Step 1: libuv Event Loop Phase Architecture

The event loop runs through these phases on each iteration ("tick"):

   ┌──────────────────────────┐
   │         timers           │  ← setTimeout / setInterval callbacks
   └──────────┬───────────────┘

   ┌──────────▼───────────────┐
   │    pending callbacks     │  ← I/O callbacks deferred from prev tick
   └──────────┬───────────────┘

   ┌──────────▼───────────────┐
   │      idle / prepare      │  ← internal use only
   └──────────┬───────────────┘

   ┌──────────▼───────────────┐
   │          poll            │  ← retrieve I/O events, run I/O callbacks
   └──────────┬───────────────┘

   ┌──────────▼───────────────┐
   │          check           │  ← setImmediate callbacks
   └──────────┬───────────────┘

   ┌──────────▼───────────────┐
   │     close callbacks      │  ← socket.on('close'), etc.
   └──────────────────────────┘

Between every phase, Node.js drains the microtask queues:

  1. process.nextTick queue (highest priority)

  2. Promises / queueMicrotask queue


Step 2: Phase Ordering Verification

Run: node phase-order.js

📸 Verified Output:

💡 process.nextTick fires before Promise microtasks. Both fire before any I/O or timer callbacks.


Step 3: Nested nextTick Starvation

💡 Infinite process.nextTick recursion starves I/O. Always use setImmediate for truly deferring work.


Step 4: setImmediate vs setTimeout in I/O Context

💡 Inside an I/O callback, setImmediate always runs before setTimeout(fn, 0) because we're already past the timers phase.


Step 5: async_hooks — Tracking Async Context

💡 async_hooks are powerful but have ~5-10% overhead. Disable in production or use AsyncLocalStorage instead.


Step 6: AsyncLocalStorage for Request Context

💡 AsyncLocalStorage is the production-safe way to propagate request IDs, user sessions, and trace spans through async call stacks.


Step 7: Event Loop Blocking Detection

💡 Libraries like clinic.js and @nicolo-ribaudo/event-loop-lag measure event loop lag in production. Alert at > 100ms.


Step 8: Capstone — Microtask Queue Visualizer

Build a complete phase visualizer showing all queue orderings:

Expected output order:


Summary

Queue / Phase
Priority
Use Case

process.nextTick

1st (highest)

Defer within same phase, before I/O

Promise microtasks

2nd

Async/await continuations

queueMicrotask

2nd (same)

Explicit microtask scheduling

Timers phase

3rd

setTimeout / setInterval

Poll phase

4th

Network, file I/O callbacks

Check phase

5th

setImmediate

AsyncLocalStorage

N/A

Request context propagation

async_hooks

N/A

Low-level async lifecycle tracking

Last updated