Event Loop Lab (Interactive)
Step a Node-style loop tick-by-tick and drop in a 200ms sync task to watch queue lag spike. An executable model of call stack, microtask queue, macrotask queue, and libuv timers showing exact callback ordering and event loop lag.
Event Loop: Microtasks, Macrotasks & Blocking
Step a Node.js-style loop tick by tick — then drop a synchronous crypto task on the stack and watch every timer starve.
Synchronous phase: frame 1/5
Call Stack (single V8 thread)
libuv / kernel (epoll, thread pool)
no pending async handles
Microtask Queue — drains COMPLETELY per tick
empty
Macrotask Queue — ONE per tick (timers, I/O, setImmediate)
empty
Press Next Tick to run the loop.
Order proof: microtasks (3, then nested 7) run before the next macrotask (4); the 200ms sync hog pushes every timer callback behind it, producing 0ms of loop lag — in production that means health-check timeouts and CrashLoopBackOff. Offload CPU work to worker_threads or a task queue.
How It Works Under the Hood
An event loop interleaves independent tasks on one thread: synchronous frames run first, then the microtask queue drains completely (promises, nextTick), then exactly one macrotask (timers, I/O callbacks) runs. Network I/O rides the kernel (epoll) while file/DNS/crypto work uses libuv’s 4-thread pool. Because everything shares the one thread, any synchronous CPU hog stalls every connection — timers scheduled at 0ms fire 200ms late.
Core Architectural Principles
- Microtasks drain to empty between every macrotask — nested promises run before the next timer.
- One macrotask executes per loop tick, so timers/I/O callbacks take turns, never concurrently.
- Loop lag equals scheduled-fire time minus actual execution time under a synchronous CPU hog.
Answer ordering puzzles by naming the queues: "stack empties, then all microtasks, then one macrotask." Then design-taste it: "Node doubles PayPal’s requests per box for I/O-bound APIs, but bcrypt.hashSync in a request handler fails health checks — offload to worker_threads or BullMQ."
Single-threaded loops give lock-free simplicity and massive I/O concurrency, but one blocking handler degrades every connection behind it.