Menu

Node.js Event Loop: Phases, nextTick and setImmediate

The Node.js event loop runs your callbacks in phases (timers, poll, check, close), and between every callback it empties the process.nextTick queue and then the promise queue. Run the examples to see the exact order.

This page includes runnable editors. Edit, run, and see output instantly.

The Node.js event loop is the loop that decides which callback runs next once your synchronous code has finished. It goes through phases in a fixed cycle: timers (setTimeout, setInterval), pending callbacks, poll (I/O such as file reads and network data), check (setImmediate) and close callbacks. After every callback, Node first empties the process.nextTick queue and then the promise queue, so those always run before the loop moves on.

It prints sync, nextTick, promise, setTimeout, the reverse of the order they were written. Synchronous code always finishes first, then Node empties the nextTick queue, then the promise queue, and only then does the loop start and reach the timers phase.

The phases of the event loop

Node's loop comes from libuv, the C library Node uses for I/O. Before the first turn it runs the timers that are already due, and then each turn runs these phases in order:

  1. Pending callbacks: a few system callbacks deferred from the previous turn, such as some network errors.
  2. Poll: new I/O events and their callbacks (a file read finished, data arrived on a socket). When nothing else is scheduled, the loop waits here.
  3. Check: setImmediate callbacks.
  4. Close callbacks: 'close' events, like a socket that was destroyed.
  5. Timers: callbacks of setTimeout and setInterval whose time has come.

Many guides list timers first in every turn, which is how Node worked before version 20 (libuv 1.45). The callbacks run in the same order either way: a turn's timers are followed by the next turn's poll.

Two queues sit outside the phases: the nextTick queue and the microtask queue (promise callbacks and queueMicrotask). Node drains both after every single callback, in any phase, nextTick first. The loop keeps turning while there is something to wait for (a timer, an open file or socket, a server) and the process exits when nothing is left.

This is Node's own loop. The browser shares the idea of tasks and microtasks but has no phases, no setImmediate and no process.nextTick; the JavaScript event loop page covers the language-level picture.

process.nextTick vs promises

Both run before the loop continues, but they are not mixed. Node runs the whole nextTick queue, including ticks added while it runs, and only then the promise queue. Predict this one:

The order is tick 1, tick 2, tick from tick, promise 1, promise 2. The tick scheduled inside a tick joins the same queue and still runs before any promise, even though the promise was created first.

At the top level of an ES module (a file that uses import) the two swap: the module's code itself runs inside a promise job, so its promise callbacks finish before Node gets to the nextTick queue. Inside callbacks, the nextTick-first rule holds in both module systems.

setImmediate vs setTimeout(fn, 0)

setTimeout(fn, 0) really means "after at least 1 ms, in the timers phase". setImmediate(fn) means "in the next check phase". Which one runs first depends on where you call them.

Inside an I/O callback the order is fixed. The callback runs in the poll phase, the check phase comes right after it, and the timers phase only comes after that:

It prints nextTick, setImmediate, setTimeout, every time. That makes setImmediate the right choice for "run this after the current I/O work".

In the main module, outside any callback, the order of the two is not fixed:

// Order depends on how fast the process started
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

If at least 1 ms has passed when the loop starts, the timer is due and prints first; otherwise setImmediate wins. Do not write code that depends on either outcome.

RunsUse for
process.nextTick(fn)Right after the current operation, before promisesEmitting an event or error after the caller has attached listeners
Promise.then, queueMicrotaskAfter the nextTick queue is emptyOrdinary async code
setImmediate(fn)Check phase, after I/O callbacksYielding to the loop between chunks of work
setTimeout(fn, 0)Timers phase, at least 1 ms laterDelays, where the exact moment does not matter

Blocking the event loop

Because there is one JavaScript thread, a callback that runs for a long time stops everything else: timers, I/O callbacks and, in a server, every other request. A timer's delay is a minimum, not a guarantee.

loop finished prints first, then true: the timer was due after 10 ms but could not run until the busy loop gave the thread back. In a server, the same loop inside a route handler freezes every other request for half a second.

The fix for long work is to split it and give the loop a turn between the pieces with setImmediate:

The timer gets in after part 1. Replace setImmediate(() => work(part + 1)) with work(part + 1) and run it again: all three parts run in one go and timer prints last.

For work that is heavy all the way through, such as image processing, splitting is not enough: move it off the main thread with worker_threads.

Common mistakes

Calling process.nextTick recursively. A nextTick callback that keeps scheduling another nextTick never lets the loop continue, so timers and I/O starve. Use setImmediate for repeated work.

Using sync APIs in a server. fs.readFileSync, crypto.pbkdf2Sync and big JSON.parse calls block the loop for every user. Use the async versions inside request handlers.

Expecting setTimeout(fn, 0) to run right away. It runs after all synchronous code, every nextTick and every promise callback, and only once the loop reaches the timers phase.

Frequently Asked Questions

Is Node.js single-threaded?

Your JavaScript runs on one thread, so two callbacks never run at the same time. Node itself uses more threads: libuv keeps a pool (four threads by default) for file system work, DNS lookups and some crypto, and hands the results back to the loop.

When does a Node.js program exit?

When the event loop has nothing left to wait for: no timers, no pending I/O, no open servers or sockets. A running setInterval or a listening server keeps the process alive until you clear or close it.

Is the Node.js event loop the same as the browser's?

The idea is the same, one thread running queued callbacks, but the details differ. The browser has no setImmediate or process.nextTick and fits rendering between tasks, while Node runs libuv's phases. The JavaScript event loop page covers the shared, language-level part.

How can I tell if the event loop is blocked?

Measure how late it runs: monitorEventLoopDelay() from perf_hooks records the delay, and a mean in the tens of milliseconds means some callback holds the thread too long. Run Node with --cpu-prof to see which function it is.

Coddy programming languages illustration

Learn to code with Coddy

Get started