Understanding the Execution Order of setTimeout, setImmediate, and process.nextTick in Node.js

Published February 20, 2021

Environment

node: v12.16.2

Node.js provides several APIs for scheduling asynchronous work, including setTimeout, setInterval, setImmediate, and process.nextTick.

They are scheduled in different parts of the event loop, which means their execution order can sometimes be surprising.

This post walks through how these APIs behave and why their ordering is not always obvious.


The Event Loop Phases

To understand the behavior of these APIs, it’s helpful to first look at the simplified structure of the Node.js event loop.

timers

pending callbacks

idle / prepare

poll

check (setImmediate)

close callbacks

Two phases are particularly relevant here:

  • timers phasesetTimeout, setInterval
  • check phasesetImmediate

Additionally, process.nextTick runs outside of these phases.


setTimeout vs setInterval

Both setTimeout and setInterval are executed in the timers phase.

Their order depends primarily on the delay value provided.

  • Callbacks with shorter delays run first.
  • If the delays are the same, the callback that was registered earlier typically executes first.

Example:

const timer = setInterval(() => {
  console.log('interval')
  clearInterval(timer)
}, 1)

setTimeout(() => {
  console.log('timeout')
}, 1);

/* ouput:
 * interval
 * timeout
 */

setTimeout vs setImmediate

setImmediate callbacks are executed in the check phase, while the callbacks of setTimeout are executed in the timers phase**, looks like setTimeout stands in the first position, right? But in many situations this leads to the following order:

setImmediate → SetImmediate

Look at the following example:

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

After registering two callbacks, the program enters the event loop.

At this moment, if:

  • the **timer ** has not yet reached its scheduled time
  • the poll queue is empty
  • the **setImmediate queue has pending tasks

then the poll phase behaves as follows:

poll phase

detects setImmediate callbacks exist

skips waiting and go directly to the check phase

executes setImmediate callbacks

Then, in the next iteration of the event loop:

timers phase

executes timeout callback

So the final execution order becomes:

immediate
timeout

However, things become different when adding the following code.

Consider this example:

setTimeout(() => {
  console.log('timeout')
})

const start = process.hrtime.bigint();
while (process.hrtime.bigint() - start < 1_000_000n) {}

setImmediate(() => {
  console.log('immediate')
})

Output

timeout
immediate

Why does this happen?

Because this piece of synchronous blocking code gives setTimeout enough time to reach the timer ready state, so in this loop, the sequence will be:

timers phase

timeout

poll

check

immediate

But This kind of busy waiting is generally not recommended in real-world projects, because it blocks the event loop and fully occupies a CPU core. If you want to guarantee the execution order, use direct nesting:

setTimeout(() => {
  console.log('timeout')
  setImmediate(() => {
    console.log('immediate')
  })
}, 0)

process.nextTick

process.nextTick() behaves differently from the other APIs.

Although it is asynchronous, it does not belong to any event loop phase.

Instead, Node.js maintains a nextTick queue, which is processed immediately after the current operation finishes, before the event loop continues.

This means process.nextTick() always runs before timers and immediates.

Example:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B() {
    console.log(2);
  });
});

setTimeout(function timeout() {
  console.log('TIMEOUT');
});

Output:

1
2
TIMEOUT

The entire nextTick queue is drained before the event loop proceeds to the timers phase.


Practical Takeaways

A few practical guidelines:

  • Use process.nextTick when work must run immediately after the current operation.
  • Use setImmediate when you want to schedule work after I/O callbacks.
  • Use setTimeout when you need actual time-based scheduling.

In practice, most applications do not rely on the exact ordering between setTimeout(0) and setImmediate. However, when debugging asynchronous behavior or writing low-level libraries, these distinctions become important.

Comments