Node.js event loop interview questions, with runnable answers
Eight event loop questions Node.js interviewers ask, with clear answers and code you can run: phases, nextTick vs promises, setImmediate and the thread pool.
Updated 8 min read

In this article
Event loop questions appear in almost every Node.js interview because they separate people who have used Node.js from people who understand why their server stalls. The short answer interviewers want: Node.js runs your JavaScript on one thread, hands I/O to the operating system or the libuv thread pool, and processes completed work in a fixed sequence of phases, draining process.nextTick and promise callbacks between each callback.
Below are the eight questions that come up most often, each with a short answer you can say out loud and code you can run to prove it. Every snippet was run on Node.js 24.19.0 and the outputs shown are real. The phase descriptions follow the official Node.js event loop guide.
1. What is the event loop, in one minute?
Node.js executes JavaScript on a single main thread. When your code starts a network request, reads a file or sets a timer, Node.js registers the operation and moves on. The event loop is the mechanism, provided by the libuv library, that waits for those operations to finish and then runs their callbacks one at a time.
That design gives Node.js high concurrency for I/O with very little memory per connection. The cost is that only one callback runs at any moment. A callback that computes for 200 ms delays every other request by 200 ms.
A good one-line answer: "Node.js is single-threaded for JavaScript, multi-threaded underneath, and the event loop decides when each completed operation's callback runs."
2. What are the phases of the event loop?
Each iteration of the loop passes through the same phases, and each phase has a first-in, first-out queue of callbacks.
| Phase | What runs there |
|---|---|
| timers | Callbacks scheduled by setTimeout() and setInterval() whose time has passed |
| pending callbacks | Some deferred system callbacks, such as certain TCP errors |
| idle, prepare | Internal use only |
| poll | Retrieves new I/O events and runs I/O callbacks; Node.js blocks here when there is nothing else to do |
| check | setImmediate() callbacks |
| close callbacks | Close handlers such as socket.on('close', ...) |
Two details earn extra credit. First, a timer's delay is a minimum, not a guarantee: the callback runs in the first timers phase after the delay has passed. Second, the official guide notes that since Node.js 20 (libuv 1.45.0) timers are run after the poll phase rather than before it, which is why older diagrams look slightly different.

3. In what order will this code log?
This is the classic whiteboard question. Save it as order.cjs and run it.
// order.cjs
console.log('sync');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));$ node order.cjs
sync
nextTick
promise
immediate
timeoutThe reasoning to say aloud:
- Synchronous code runs first, to completion.
- The
nextTickqueue is drained next, then the promise microtask queue. Both run before the event loop moves on. setTimeout(0)versussetImmediate()in the main module is not guaranteed. The official guide calls the order non-deterministic because it depends on process performance. It printedimmediatefirst in our runs, but you should not rely on that.
The follow-up most candidates miss: ES modules
Rename the file to order.mjs, add a second microtask, and the order of nextTick and promises flips:
// order.mjs
console.log('1 sync start');
setTimeout(() => console.log('6 timeout'), 0);
setImmediate(() => console.log('7 immediate'));
Promise.resolve().then(() => console.log('4 promise'));
queueMicrotask(() => console.log('5 microtask'));
process.nextTick(() => console.log('3 nextTick'));
console.log('2 sync end');$ node order.mjs
1 sync start
2 sync end
4 promise
5 microtask
3 nextTick
7 immediate
6 timeoutAn ES module is evaluated asynchronously, so its top-level code is itself running inside promise machinery. Microtasks queued there run before Node.js gets a chance to drain the nextTick queue. The practical lesson: do not write code whose correctness depends on the relative order of nextTick and promise callbacks.
4. What is the difference between process.nextTick() and setImmediate()?
The names are famously backwards, and the Node.js documentation admits it.
process.nextTick()is not part of the event loop. Its queue is processed after the current operation completes, before the loop continues, whatever phase it is in.setImmediate()runs in the check phase, on the following iteration of the loop.
The danger with nextTick is starvation: if a nextTick callback schedules another nextTick, recursively, the loop never reaches the poll phase and no I/O is processed. The official guide recommends using setImmediate() in all cases because it is easier to reason about. nextTick remains useful for one thing: emitting an event or calling a callback after the current function returns, so callers have time to attach listeners.
5. setTimeout(fn, 0) or setImmediate(fn): which runs first?
It depends where you call them. In the main module the order is not guaranteed. Inside an I/O callback, setImmediate() always runs first, because the loop goes from the poll phase straight to the check phase before it comes back round to timers.
// io.cjs
const fs = require('node:fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});$ node io.cjs
immediate
timeout6. If Node.js is single-threaded, what is the thread pool for?
Some work has no non-blocking operating system API, so libuv runs it on a fixed pool of threads. According to the Node.js CLI documentation, the thread pool handles all fs APIs except file watchers and explicitly synchronous ones, asynchronous crypto such as crypto.pbkdf2(), crypto.scrypt() and crypto.randomBytes(), dns.lookup(), and all asynchronous zlib APIs. The pool has 4 threads by default, set by the UV_THREADPOOL_SIZE environment variable.
You can see the default size in action. Eight expensive hashes complete in two waves of four:
// pool-size.mjs
import { pbkdf2 } from 'node:crypto';
const start = performance.now();
for (let i = 1; i <= 8; i++) {
pbkdf2('secret', 'salt', 600_000, 64, 'sha512', () => {
console.log(`hash ${i} done after ${Math.round(performance.now() - start)} ms`);
});
}$ node pool-size.mjs
hash 4 done after 320 ms
hash 1 done after 321 ms
hash 3 done after 326 ms
hash 2 done after 329 ms
hash 5 done after 513 ms
hash 6 done after 516 ms
hash 7 done after 530 ms
hash 8 done after 541 ms
$ UV_THREADPOOL_SIZE=8 node pool-size.mjs
# all eight finish between 262 and 290 msTimings are from one machine and will differ on yours; the two-wave pattern will not. Set the variable when the process starts. The documentation warns that assigning process.env.UV_THREADPOOL_SIZE from inside your code is not guaranteed to work, because the pool may already exist. Network sockets do not use this pool, which is why thousands of open connections do not need thousands of threads.

7. How would you detect that the event loop is blocked?
Measure event loop delay. monitorEventLoopDelay() from node:perf_hooks samples how late a timer fires and reports a histogram in nanoseconds. Here a 200 ms busy loop shows up immediately:
// block.mjs
import { monitorEventLoopDelay } from 'node:perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
setTimeout(() => {
const start = Date.now();
while (Date.now() - start < 200) {} // block the loop for 200 ms
}, 100);
setTimeout(() => {
histogram.disable();
console.log('p99 delay ms:', (histogram.percentile(99) / 1e6).toFixed(1));
console.log('max delay ms:', (histogram.max / 1e6).toFixed(1));
}, 500);$ node block.mjs
p99 delay ms: 202.4
max delay ms: 202.4In production, export the p99 delay to your metrics system and alert on it. performance.eventLoopUtilization() is the complementary signal: the share of time the loop spends doing work rather than waiting for events. For finding the guilty function, take a CPU profile with node --cpu-prof and open it in Chrome DevTools.
8. How do you fix a handler that blocks the loop?
The official "Don't block the event loop" guide gives two strategies, and a strong answer names both:
- Partition the work: process a large array in chunks and yield between chunks with
setImmediate(), so other callbacks can run. - Offload the work: move CPU-heavy tasks to
worker_threads(ideally a fixed-size pool) or to a separate service. Offloading costs serialisation, so it suits large tasks.
Then mention the less obvious blockers the guide calls out: regular expressions with nested quantifiers such as (a+)* that can take exponential time (ReDoS), synchronous APIs such as fs.readFileSync() in request handlers, and JSON.parse() or JSON.stringify() on very large payloads. Bound the size of anything a client can send.
Gotchas interviewers like to probe
- "async" does not mean "off the main thread". An
asyncfunction that loops over a million items still blocks;awaitonly yields when it waits on something. - Timer delays are minimums.
setTimeout(fn, 100)during a 200 ms block fires after about 200 ms. - Unhandled promise rejections crash the process by default in current Node.js versions, so every promise chain needs an error path.
- Thread pool saturation looks like slow I/O. Heavy
pbkdf2orzlibuse can delay unrelatedfscalls and DNS lookups.
What to do next
Run each snippet above and change one thing at a time: move the setTimeout/setImmediate pair inside an I/O callback, add recursive nextTick calls, raise UV_THREADPOOL_SIZE. Being able to predict and then explain the output is exactly what an interviewer is testing. After that, read the official event loop guide end to end; it is short, and quoting it accurately is more convincing than any diagram.
Frequently asked questions
Is Node.js single-threaded?
Your JavaScript runs on one main thread, but Node.js is not single-threaded overall. libuv uses a thread pool (four threads by default) for file system, DNS lookup, some crypto and zlib work, and you can start more threads yourself with
worker_threads.Does process.nextTick run before promises?
In a CommonJS script, yes: the
nextTickqueue is drained before the promise microtask queue. In an ES module the top-level code runs inside promise machinery, so promise callbacks queued there can run first. Do not depend on the relative order.Which runs first, setTimeout(fn, 0) or setImmediate(fn)?
In the main module the order is non-deterministic. Inside an I/O callback,
setImmediate()always runs first, because the check phase comes straight after the poll phase.What is the default size of the libuv thread pool?
Four threads. Change it with the
UV_THREADPOOL_SIZEenvironment variable when the process starts, for exampleUV_THREADPOOL_SIZE=8 node server.js.How do I measure event loop lag in production?
Use
monitorEventLoopDelay()fromnode:perf_hooksand export its p99 value (reported in nanoseconds) to your metrics system. Pair it withperformance.eventLoopUtilization().Does using async/await stop code from blocking the event loop?
No.
awaitonly yields while it waits for a promise. CPU-heavy synchronous work inside anasyncfunction still blocks every other request until it finishes.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
More from Cloudsoft Zone

Industry updateNode.js
Which Node.js version to use in October 2026: LTS lines and the new schedule
Node.js 26 becomes LTS on 28 October 2026, Node.js 24 moves to maintenance and 20.x is already end of life. Here is every supported line, the new yearly release model from Node.js 27, and which version to run.
By Cloudsoft Zone

ComparisonNode.js
Express 5 vs Fastify 5: which Node.js framework to pick in 2026
Express 5 and Fastify 5 compared on support status, validation, error handling, upgrades and performance, with the same API built and tested in both.
By Cloudsoft Zone

TutorialNode.js
How to test a Node.js API with the built-in test runner (node:test)
Write, run, mock and measure coverage for a small Node.js HTTP API using only node:test and node:assert. Covers port-0 servers, fetch(), spies, t.mock.method(), fake dates and coverage thresholds.
By Cloudsoft Zone
