Know the Loop's Phases and You'll Know Why Your Algorithm Blocks
The Node.js Event Loop
A deep look at the six phases of the Node.js event loop — timers, pending callbacks, poll, check, close — and why a tight algorithmic loop stalls every I/O callback in your server.
What you'll learn
- Name the six phases of the Node.js event loop in order
- Explain where microtasks (Promise callbacks and queueMicrotask) fit in the loop
- Identify why a synchronous algorithm that runs too long blocks I/O handling
The Node.js event loop is the engine that keeps a single-threaded process responsive while waiting on I/O. Every algorithm you run in Node competes with network sockets, timers, and file reads for that one thread — understanding the loop’s phases tells you exactly when your code will block and when it won’t.
The Six Phases
The loop visits six phases on every iteration (“tick”):
| Phase | Handles |
|---|---|
| Timers | setTimeout and setInterval callbacks whose delay has expired |
| Pending callbacks | I/O callbacks deferred from the previous iteration |
| Idle / prepare | Internal Node bookkeeping (not user-visible) |
| Poll | Retrieve new I/O events; blocks here when the queue is empty |
| Check | setImmediate callbacks |
| Close callbacks | socket.on('close', …) and similar cleanup handlers |
After every phase transition — and after every individual callback — Node
drains the microtask queue: first all process.nextTick callbacks, then
all resolved Promise .then / catch / finally handlers.
Microtasks Beat Macrotasks
// Run in Node to see the order for yourself
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise 1'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise 2'));
console.log('sync');
// Output:
// sync
// nextTick
// promise 1
// promise 2
// timeout process.nextTick runs before any Promise callback. Both run before the timer
phase fires — even when the timer delay is 0.
Why a Tight Loop Blocks Everything
The poll phase can only retrieve new network data when Node is there. If your algorithm takes 400 ms of synchronous CPU work, the loop cannot visit the poll phase for those 400 ms. Every waiting HTTP request, every database response, every file read sits in limbo.
import { createServer } from 'node:http';
const server = createServer((_req, res) => {
// Simulates an expensive synchronous algorithm
const end = Date.now() + 400;
while (Date.now() < end) { /* busy-wait */ }
res.end('done');
});
server.listen(3000);
// Every request serialises; concurrent clients all wait in line. Algorithm Choice Is an Event-Loop Choice
This is why the data-structure and algorithm choices you make in a Node service are architectural decisions, not just academic ones:
- O(n log n) sort on 10 000 items — finishes in a millisecond, invisible to the loop.
- O(n²) nested loop on 100 000 items — tens of seconds; every user feels it.
- Recursive DFS on a deep graph — risks blowing the call stack and blocking the loop.
Knowing the loop makes the performance cost of a naive algorithm concrete, not theoretical.
Up Next
Learn how to measure the cost of synchronous work and how to break it into
non-blocking chunks with setImmediate.