setTimeout(0) vs setImmediate(): The Event Loop Trap That Exposes Senior Engineers
setTimeout(0) vs setImmediate(): The Event Loop Trap That Exposes Senior Engineers
A common misconception in Node.js is that setTimeout(fn, 0) means “run fn immediately.”
Another is that setImmediate(fn) is just a faster version of the same idea.
Neither is true.
The difference isn’t about speed—it’s about which event loop phase your callback is queued into. If you understand that, the behavior becomes predictable.
The Problem
Consider:
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Which one logs first?
A lot of people answer “setTimeout”.
In practice, that’s unreliable.
Outside of I/O, the ordering is not guaranteed.
Why It Happens
Node.js doesn’t “run async code”; it schedules callbacks into specific phases of the event loop.
The key mapping is:
setTimeout/setIntervalgo to the timers phase (when the delay has elapsed)setImmediategoes to the check phase
Those phases are separated by other work (notably the poll phase), so the order depends on what the loop does next.
Internal Architecture
The simplified phase sequence relevant here:
flowchart LR
timers --> pending
pending --> poll
poll --> check
check --> close
Roughly:
- timers phase executes expired timers (
setTimeout,setInterval) - poll phase waits for and runs I/O callbacks
- check phase runs
setImmediatecallbacks
The detail that causes confusion
After the poll phase, Node decides:
- if the poll queue is empty (and it’s not going to block waiting for I/O), it can move to check
- otherwise it keeps polling and only reaches check later
That decision, plus whether the timer is considered “expired” yet, is why the ordering can vary.
Example Scenario
Case 1: Outside I/O (non-deterministic)
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Both outputs are possible:
timeout
immediate
or:
immediate
timeout
The exact ordering depends on how the event loop progresses and whether the timer is due by the time Node reaches the timers phase.
Case 2: Inside I/O (deterministic)
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});
Output:
immediate
timeout
This ordering is reliable.
Diagram
Outside I/O (race)
sequenceDiagram
participant JS
participant EventLoop
JS->>EventLoop: setTimeout(0)
JS->>EventLoop: setImmediate()
Note over EventLoop: Ordering depends on loop state and timing
EventLoop->>JS: timers phase OR check phase
Inside I/O (deterministic ordering)
sequenceDiagram
participant I/O
participant Poll
participant Check
participant Timers
I/O->>Poll: Callback ready
Poll->>Check: setImmediate runs
Check->>Timers: Next loop
Timers->>JS: setTimeout runs
The Real Reason (Senior Insight)
setImmediate() is designed to run after the poll phase, in the check phase.
When you schedule it from within an I/O callback, you’re already anchored to the poll phase. Node finishes poll, transitions to check, and runs setImmediate before the next iteration’s timers phase.
That’s why, inside I/O callbacks, setImmediate consistently runs before setTimeout(…, 0).
Outside of I/O, there’s no such anchor; the event loop’s timing and state determine whether timers fire before check.
Key Takeaways
setTimeout(fn, 0)is not “immediate”; it runs in the timers phase after the delay is considered elapsed.setImmediate(fn)is not “faster”; it runs in the check phase.- Outside I/O, the ordering between the two is not guaranteed.
- Inside I/O callbacks,
setImmediatereliably runs first. - The difference is phase scheduling, not raw timing.
Final Thoughts
This isn’t just trivia.
At scale, knowing when callbacks run affects:
- batching and fairness
- latency and tail behavior
- backpressure and I/O throughput
When you can explain the phase ordering, the behavior stops being “weird Node.js magic” and becomes a predictable system.
Next in the Series
Continue reading: The Hidden Priority System in Node.js: Why nextTick Can Starve Your Entire App