Skip to main content

Command Palette

Search for a command to run...

Why Does setImmediate Sometimes Run Before setTimeout(fn, 0)?

Published
8 min read
Why Does setImmediate Sometimes Run Before setTimeout(fn, 0)?

Author: Saurabh Prajapati · Software Engineer @ IBM India · May 2025


The Mystery That Started This

I was writing some async Node.js code and stumbled across something that genuinely confused me. Look at this snippet:

setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));

I ran it and got:

setImmediate
setTimeout

Wait… what? I set the timeout to 0 milliseconds. That means "run immediately," right? So why did setImmediate run first?

Then I added one line — just a console.log at the top — and everything flipped:

console.log("Hello from console");
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));

Output:

Hello from console
setTimeout
setImmediate

Now setTimeout runs before setImmediate. How?! Both snippets look almost identical. This drove me crazy until I finally understood what Node.js is actually doing under the hood.

Let me walk you through exactly what I learned.


First, Let's Understand the Node.js Event Loop

Before we can solve this mystery, we need a mental model of how Node.js handles async code. Node.js is single-threaded, which means it can only do one thing at a time. But it can schedule things to happen later using the event loop.

Think of the event loop like a to-do list manager. It has different trays for different kinds of tasks, and it processes each tray in a specific order — every single tick.

The simplified order of phases looks like this:

┌─────────────────────────────────┐
│         timers phase            │  ← runs setTimeout / setInterval callbacks
├─────────────────────────────────┤
│      pending callbacks          │
├─────────────────────────────────┤
│        idle / prepare           │
├─────────────────────────────────┤
│          poll phase             │  ← waits for I/O, fetches new events
├─────────────────────────────────┤
│         check phase             │  ← runs setImmediate callbacks  ✅
├─────────────────────────────────┤
│      close callbacks            │
└─────────────────────────────────┘
         ↑ loops back up ↑

Two phases matter most for our mystery:

Timers phase is where setTimeout and setInterval callbacks live. Node checks: "Has enough time passed for this timer to be due?" If yes, it runs the callback.

Check phase is where setImmediate callbacks live. This phase always runs after the poll phase, every single loop iteration.

So the order in the loop is always: timers → ... → poll → check. That means setTimeout callbacks are checked before setImmediate callbacks in the phase order. You'd expect setTimeout to always win, right?

This is where it gets interesting.


The Real Culprit: Timer Precision

Here's the thing about setTimeout(fn, 0) — it doesn't actually mean "zero milliseconds." It means "run this after at least 0 milliseconds." The minimum delay in Node.js is technically 1ms, and even that isn't guaranteed because reading the system clock takes a tiny amount of time.

So when Node.js starts the event loop and reaches the timers phase, it asks the system clock: "Is the timer ready?" If that clock read happens to be just a hair too early — say the timer was registered 0.3ms ago and the minimum is 1ms — Node says "not yet" and skips the setTimeout callback for this tick.

Then the event loop continues to the check phase, runs setImmediate, and only on the next tick does it come back to timers and find that setTimeout is now ready.

This is why the output of the first snippet is non-deterministic — you might even see it flip if you run it multiple times. It entirely depends on the timing of when Node reads the clock.


So Why Does Code 2 Always Produce a Consistent Order?

This is the key insight. When you add console.log("Hello from console") at the top, you're running that code synchronously — it executes before the event loop even starts. The setTimeout and setImmediate are registered after that synchronous work completes.

By the time Node.js finishes the synchronous code and enters the event loop, a small but real amount of time has already passed. That extra time is almost always enough for the 1ms minimum timer threshold to be comfortably satisfied.

So when the event loop reaches the timers phase, it checks: "Is the setTimeout(0) ready?" And now the answer is yes — because enough real time has passed. It runs the setTimeout callback first, then moves on to the check phase and runs setImmediate.

Think of it this way:

No sync work before them → timer might not be ready → setImmediate can sneak in first → order is unpredictable.

Sync work before them → timer is definitely ready by the time event loop starts → setTimeout runs first → order is predictable.


Let's Cement This With the Timeline

Here's exactly what happens in Code 1 (no prior sync work):

1. Node.js starts the event loop almost immediately
2. Enters TIMERS phase → checks setTimeout(0)
   → "Is 1ms elapsed?" → Maybe not yet! → SKIPPED
3. Enters CHECK phase → runs setImmediate  ← prints "setImmediate"
4. Next iteration → TIMERS phase again
   → "Is 1ms elapsed?" → YES now → runs setTimeout  ← prints "setTimeout"

And here's what happens in Code 2 (with a sync console.log first):

1. Synchronous code runs: console.log("Hello from console")  ← prints immediately
   (This takes a few microseconds of real time)
2. Node.js enters the event loop
3. Enters TIMERS phase → checks setTimeout(0)
   → "Is 1ms elapsed?" → YES, sync work already used that time → runs it  ← prints "setTimeout"
4. Enters CHECK phase → runs setImmediate  ← prints "setImmediate"

One More Thing

If you call setTimeout and setImmediate inside an I/O callback, the order becomes always predictablesetImmediate will always run before setTimeout, no matter what. That's because I/O callbacks fire during the poll phase, and the check phase comes right after poll in every single loop iteration.

const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => console.log("setTimeout"), 0);
  setImmediate(() => console.log("setImmediate"));
});

Output is always:

setImmediate
setTimeout

Inside an I/O callback, you're already past the timers phase. So setTimeout cannot possibly run until the next loop iteration. But setImmediate runs in the very next phase (check), so it always wins here.

This is why setImmediate is the recommended way to schedule something "right after the current I/O cycle" — it's predictable and reliable inside async callbacks.


The Final Answer

Let's bring it all together. The two snippets behave differently because:

Code 1 gives Node.js almost no time before the event loop starts. The setTimeout(0) timer might not have crossed the 1ms minimum threshold by the time the timers phase runs, so setImmediate (which lives in the check phase, guaranteed to run every loop) gets a chance to run first. This makes the output non-deterministic.

Code 2 runs a synchronous console.log first. That tiny bit of real-world execution time is enough for the timer to be considered ready by the time the event loop's timers phase runs. So setTimeout reliably fires first, followed by setImmediate. The output is predictable.

The deeper lesson is this: setTimeout(fn, 0) does not mean "run right now." It means "run as soon as possible after at least 1ms." The event loop's timer phase simply checks whether the delay has been satisfied — and sometimes the answer is "not quite yet."


Quick Reference

setTimeout(fn, 0) setImmediate(fn)
Phase Timers Check
Delay guarantee At least ~1ms After current poll phase
Order vs each other Non-deterministic at top level Non-deterministic at top level
Inside I/O callback Runs next iteration Always runs first
Use when Delaying by time Scheduling after I/O

Try It Yourself

Run both snippets a few times and watch the output of Code 1 vary between runs — you might see it flip. Then wrap them inside a fs.readFile callback and notice that setImmediate always wins. Playing with it hands-on is what made this truly click for me.


About the Author

Saurabh Prajapati is a Full-Stack Software Engineer at IBM India Software Lab, working on Maximo — cloud-native enterprise solutions. He specializes in GenAI, React, and modern web technologies, and loves documenting his learning journey so others don't have to stumble on the same confusing bugs.

Connect with him: GitHub — prajapatisaurabh · LinkedIn — saurabh-prajapati · saurabhprajapati120@gmail.com