The event loop is the mechanism that makes Node.js's asynchronous, single-threaded model work. This lesson walks through its phases and explains exactly why async code executes in the order it does.
What Is the Event Loop?
The event loop is a continuously running cycle, managed by libuv, that decides which piece of ready-to-run code executes next: timers, I/O callbacks, or other queued work. It's the reason Node.js can start a file read, immediately move on to other code, and come back to the file's callback once it's ready, all on a single main thread.
Each full pass through the loop is made of distinct phases, executed in a fixed order, and Node.js also drains two special queues, microtasks (promises) and process.nextTick() callbacks, between most phases.
Output: 1, 2, 3, 4. Synchronous code always runs first, then microtasks (promises), then macrotasks (timers) on the next loop iteration.
The Event Loop Phases
┌─> timers (setTimeout, setInterval callbacks)
│ pending callbacks (I/O callbacks deferred from previous cycle)
│ poll (retrieve new I/O events, run their callbacks)
│ check (setImmediate callbacks)
└─ close callbacks ('close' event callbacks)
The loop cycles through these phases repeatedly for the life of the process.
Microtasks (promise callbacks) and process.nextTick() run between phases, not as a phase themselves.
setTimeout(fn, 0) still waits for the timers phase, it never runs synchronously.
setImmediate() runs in the check phase, generally right after the current poll phase completes.
Event Loop Cheatsheet
Execution order rules worth memorizing.
Type
Examples
Runs When
Synchronous code
top-level statements, function bodies
Immediately, before anything async
process.nextTick()
process.nextTick(fn)
Before microtasks, after the current operation
Microtasks
.then(), await
After nextTick queue, before the next macrotask
Timers
setTimeout, setInterval
Timers phase, once the delay has elapsed
I/O callbacks
fs.readFile callback
Poll phase, once the operation completes
setImmediate()
setImmediate(fn)
Check phase, generally after I/O callbacks
Microtasks vs Macrotasks
Promise callbacks (.then(), await continuations) are "microtasks": Node.js fully drains the entire microtask queue before moving on to the next macrotask (like a timer or I/O callback). process.nextTick() callbacks are drained even earlier than microtasks, giving them the highest priority of all.
Priority order: synchronous code, then process.nextTick(), then promise microtasks, then macrotasks (timers, I/O).
A microtask that queues another microtask can, in theory, starve macrotasks if it never stops.
This is why a setTimeout(fn, 0) callback can run after several chained .then() callbacks, even though the timeout "looks" like it was scheduled first.
Don't Block the Event Loop
Because Node.js runs your JavaScript on a single main thread, any long-running synchronous operation, a huge loop, JSON.parse() on a massive string, a synchronous crypto operation, blocks everything else: other requests, timers, and I/O callbacks all wait until it finishes.
// Blocks the event loop for the whole computation
function blockingFibonacci(n) {
if (n < 2) return n;
return blockingFibonacci(n - 1) + blockingFibonacci(n - 2);
}
// Better: move CPU-heavy work to a Worker Thread (covered later in this course)
For genuinely CPU-bound work, the fix isn't async syntax, it's moving the work off the main thread entirely with Worker Threads or a child process.
Common Mistakes
Assuming setTimeout(fn, 0) runs immediately, synchronously, it still waits for the timers phase.
Running CPU-intensive synchronous code inside a request handler, freezing every other connection.
Confusing microtasks (promises) with macrotasks (timers), leading to wrong assumptions about execution order.
Chaining an excessive number of process.nextTick() calls, which can starve the event loop of I/O callbacks.
Key Takeaways
The event loop, powered by libuv, decides which ready callback runs next on Node's single main thread.
Execution priority: synchronous code, then process.nextTick(), then promise microtasks, then macrotasks.
Timers, I/O callbacks, and setImmediate() each run in their own dedicated event loop phase.
CPU-bound synchronous work blocks the loop regardless of how "async" the surrounding code looks.
Pro Tip
If a server feels unresponsive under load despite "using async code everywhere," look for a hidden synchronous bottleneck, a big JSON.parse(), a synchronous regex on huge input, or a *Sync file call, rather than assuming the event loop itself is broken.
You now understand the event loop in depth. Next, learn Node.js Events and the EventEmitter pattern built on top of it.