NODE.JS GUIDE
Node.js Event Loop Explained with a Practical Example
By the HireReadyAI team · Last updated 5 October 2026 · 8 min read
The phases, where nextTick and promises fit, and a short example that trips up most candidates.
What the event loop is
Node.js runs your JavaScript on a single main thread. The event loop is what lets that one thread handle thousands of connections: slow work such as network and disk I/O is handed off to the operating system or libuv’s thread pool, and when it finishes, its callback is queued. The event loop keeps picking queued callbacks and running them, one at a time.
That is why one slow, synchronous function hurts everyone: while it runs, no other callback can.
The phases
Each turn of the loop moves through a fixed set of phases, and each phase has its own queue of callbacks:
| Phase | What runs |
|---|---|
| timers | runs expired setTimeout and setInterval callbacks |
| pending callbacks | runs some system-level I/O callbacks deferred from the last loop |
| poll | gets new I/O events and runs their callbacks; waits here if there is nothing else to do |
| check | runs setImmediate callbacks |
| close callbacks | runs close handlers, such as socket.on('close') |
Microtasks: nextTick and promises
Two queues sit outside the phases. After each callback finishes, Node empties the process.nextTick queue first, then the promise microtask queue (.then, await continuations). Only then does the loop move on.
So microtasks always run before the next timer or I/O callback, and nextTick runs before promises.
A practical example
Predict the output before reading on:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('end');
Output:
start
end
nextTick
promise
timeout (order of these two
immediate can vary, see below)
The two synchronous logs run first. Then the microtask queues drain: nextTick, then the promise. When this runs from the main module, the order of timeout and immediate depends on how quickly the loop starts, so it can change between runs. Inside an I/O callback, setImmediate always runs first, because the check phase comes straight after poll.
Common mistakes
- Saying Node.js is fully single-threaded. Your JavaScript is, but I/O and some built-ins use libuv’s thread pool.
- Assuming setTimeout(fn, 0) runs immediately. It waits at least until the timers phase.
- Calling process.nextTick recursively, which starves I/O because the loop never moves on.
- Running CPU-heavy work (large JSON parsing, crypto, image processing) on the main thread instead of worker threads.
Interview follow-up questions
- Why can recursive nextTick calls freeze a server, while recursive setImmediate calls do not?
- How would you move a CPU-heavy task off the main thread?
- What runs in libuv’s thread pool, and how do you change its size?
- How would you detect event loop lag in production?