Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
Node.jsInterview questionsJavaScriptInterview preparation

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.

Cloudsoft Zone

Updated 8 min read

Black-and-white aerial view of an oval roundabout above a multi-lane road in Belgrade, with cars and buses circling the loop
Photograph: Petar Milošević / CC BY-SA 4.0, via Wikimedia Commons
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.

PhaseWhat runs there
timersCallbacks scheduled by setTimeout() and setInterval() whose time has passed
pending callbacksSome deferred system callbacks, such as certain TCP errors
idle, prepareInternal use only
pollRetrieves new I/O events and runs I/O callbacks; Node.js blocks here when there is nothing else to do
checksetImmediate() callbacks
close callbacksClose 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.

A pink steel roller coaster with a vertical loop and helix on green supports at the Yomiuriland amusement park
Each turn of the event loop passes through timers, poll, check and close phases in the same order.

3. In what order will this code log?

This is the classic whiteboard question. Save it as order.cjs and run it.

JavaScript
// 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'));
Shell
$ node order.cjs
sync
nextTick
promise
immediate
timeout

The reasoning to say aloud:

  1. Synchronous code runs first, to completion.
  2. The nextTick queue is drained next, then the promise microtask queue. Both run before the event loop moves on.
  3. setTimeout(0) versus setImmediate() in the main module is not guaranteed. The official guide calls the order non-deterministic because it depends on process performance. It printed immediate first 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:

JavaScript
// 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');
Shell
$ node order.mjs
1 sync start
2 sync end
4 promise
5 microtask
3 nextTick
7 immediate
6 timeout

An 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.

JavaScript
// io.cjs
const fs = require('node:fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});
Shell
$ node io.cjs
immediate
timeout

6. 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:

JavaScript
// 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`);
  });
}
Shell
$ 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 ms

Timings 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.

Two interlocking iron gears of an old church tower clock mechanism mounted on a wooden frame
Like gears that can only turn so fast, the libuv thread pool has a fixed number of threads, four by default.

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:

JavaScript
// 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);
Shell
$ node block.mjs
p99 delay ms: 202.4
max delay ms: 202.4

In 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 async function that loops over a million items still blocks; await only 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 pbkdf2 or zlib use can delay unrelated fs calls 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 nextTick queue 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_SIZE environment variable when the process starts, for example UV_THREADPOOL_SIZE=8 node server.js.

  • How do I measure event loop lag in production?

    Use monitorEventLoopDelay() from node:perf_hooks and export its p99 value (reported in nanoseconds) to your metrics system. Pair it with performance.eventLoopUtilization().

  • Does using async/await stop code from blocking the event loop?

    No. await only yields while it waits for a promise. CPU-heavy synchronous work inside an async function still blocks every other request until it finishes.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. The Node.js Event Loop — Node.js
  2. Don't Block the Event Loop (or the Worker Pool) — Node.js
  3. Understanding process.nextTick() — Node.js
  4. Command-line options: UV_THREADPOOL_SIZE (Node.js v24 docs) — Node.js
  5. Performance measurement APIs (Node.js v24 docs) — Node.js

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.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.