The 6 Phases of the Event Loop — Why Your Mental Model of Node.js Is Probably Wrong
The 6 Phases of the Event Loop — Why Your Mental Model of Node.js Is Probably Wrong
Most engineers can recite the phases of the Node.js event loop.
Very few can explain what actually happens in each phase — or why it matters.
That gap leads to subtle production bugs:
- Timers firing late
- Unexpected execution order
- Throughput bottlenecks under load
This article breaks down the real behavior of the event loop — not the simplified version.
The Problem
Most explanations of the Node.js event loop look like this:
“There are phases: timers → pending callbacks → poll → check → close callbacks.”
Technically correct.
Practically misleading.
Because:
- Engineers assume phases execute uniformly
- They assume timers are precise
- They assume async callbacks run “when ready”
None of these are true.
The event loop is not a timeline — it’s a scheduling system influenced by I/O, queues, and system state.
Why It Happens
At its core, Node.js uses libuv’s event loop, which:
- Maintains multiple internal queues
- Runs them in a fixed phase order
- Delegates I/O to the OS or threadpool
- Executes callbacks only when results are ready
But here’s the key insight:
The poll phase controls everything.
Why?
Because:
- It waits for I/O
- It decides whether to block or continue
- It determines when timers/check phases execute
This means the event loop is not strictly cyclical — it’s adaptive.
Internal Architecture
Let’s look at the actual phases:
-
Timers Phase
- Executes callbacks from
setTimeoutandsetInterval - Only if their threshold has passed
- Executes callbacks from
-
Pending Callbacks
- Executes deferred system-level callbacks (e.g., TCP errors)
-
Idle / Prepare
- Internal use (libuv housekeeping)
-
Poll Phase
- Fetches new I/O events
- Executes I/O callbacks
- Can block if nothing else is scheduled
-
Check Phase
- Executes
setImmediatecallbacks
- Executes
-
Close Callbacks
- Executes cleanup callbacks (e.g., socket close)
Example Scenario
Let’s analyze a classic confusion:
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
What runs first?
Answer: It depends.
- If executed in the main script → non-deterministic
- If inside an I/O callback →
setImmediatewins
Why?
Because:
- After I/O, the loop goes to check phase
setImmediateis processed before returning to timers
Diagram
Event Loop Phases
flowchart LR
Timers --> Pending
Pending --> Idle
Idle --> Poll
Poll --> Check
Check --> Close
Close --> Timers
What Actually Happens Per Tick
sequenceDiagram
participant Loop
participant Timers
participant Poll
participant Check
Loop->>Timers: Run expired timers
Loop->>Poll: Process I/O / wait
Poll-->>Loop: Callbacks ready
Loop->>Check: Run setImmediate
Loop->>Timers: Next iteration
Key Takeaways
-
Timers are NOT precise
setTimeout(fn, 0)does not mean immediate- It means “execute after minimum threshold + loop delay”
-
Poll phase dominates execution
- If poll is busy → timers are delayed
- If poll is idle → loop may block waiting for I/O
-
setImmediate≠setTimeout(fn, 0)- Different queues
- Different phases
- Different guarantees
-
Event loop is adaptive, not fixed
-
Behavior changes based on:
- I/O load
- Queue sizes
- System timing
-
Final Thoughts
Senior engineers don’t just memorize event loop phases.
They understand:
- when phases execute
- why delays happen
- how system load reshapes behavior
The biggest mental shift is this:
Node.js is not executing your code in order — it is scheduling callbacks based on system state.
And once you internalize that, debugging async behavior becomes dramatically easier.
Next in the series: Microtasks vs Macrotasks — Why Promises Can Starve Your Event Loop
Next in the Series
Continue reading: setTimeout(0) vs setImmediate(): The Event Loop Trap That Exposes Senior Engineers