Question presented to candidate: "Node.js is single-threaded, yet it handles thousands of concurrent connections. Walk me through what actually happens between when your code calls an async function and when its callback runs."
What a strong answer should cover:
- The event loop is a loop over a fixed set of phases, each with its own queue of callbacks: timers (
setTimeout/setInterval), pending callbacks, poll (I/O events — most callbacks live here), check (setImmediate), and close callbacks. - Between every callback — not just between phases — Node drains two microtask queues:
process.nextTick()'s queue first, then the Promise microtask queue. This is whynextTickandPromise.thenalways run before the next macrotask (a timer, an I/O callback), no matter how short the timer's delay is. - 📌 The distinguishing, verifiable claim:
process.nextTick()recursion can starve the event loop entirely — since it is drained as a microtask queue, an unbounded chain ofnextTickcalls never lets the loop advance to the poll phase, so I/O callbacks never fire.setImmediate, being tied to a real phase (check), cannot cause this — the loop still cycles through poll on every iteration. - Ordering between
setTimeout(fn, 0)(timers phase) andsetImmediate()(check phase) at the top level of a script is not guaranteed — it depends on process startup timing. Inside an I/O callback, the order is guaranteed:setImmediatealways fires beforesetTimeout(fn, 0), because poll transitions to check before looping back to timers. - The single thread runs your JavaScript; actual I/O (disk, some DNS, some crypto) is delegated to libuv's thread pool or the OS's native async APIs (epoll/kqueue/IOCP for networking) — the event loop itself never blocks waiting for that work; it is notified when it completes.
- A good answer distinguishes "the event loop" (a scheduling mechanism) from "concurrency" (achieved by never blocking the single thread on I/O, not by running JS in parallel) — Node achieves throughput by staying busy between I/O waits, not by using multiple threads for your code.
Clarifying questions expected:
- "Are we talking about I/O-bound concurrency, or CPU-bound work?" — the event loop model only explains the former; CPU-bound work needs Worker Threads.
- "Do you want the phase list, or the microtask-vs-macrotask distinction specifically?" — these are related but different levels of the same answer.
Code / implementation expected: Yes — a single script combining setTimeout, setImmediate, process.nextTick, a Promise, and a real I/O callback, with the actual observed output, is the clearest way to prove the ordering rather than describe it.