Skip to solution
mediumAmazonDSA2024 · 2022

How does the Node.js event loop work?

2.9k views
01

Understand the problem

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 why nextTick and Promise.then always 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 of nextTick calls 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) and setImmediate() (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: setImmediate always fires before setTimeout(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.

nodeevent-loopasync
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js interviews — assumes basic async/callback familiarity, no prior event-loop knowledge required. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every ordering claim b

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

Full ordering proof: sync code, nextTick, promises, timers, check phase, and inside-I/O-callback ordering
const fs = require("node:fs");

console.log("1: script start");
setTimeout(() => console.log("6: setTimeout(0)"), 0);
setImmediate(() => console.log("7: setImmediate"));
fs.readFile(__filename, () => {
  console.log("A: fs.readFile callback (poll phase)");
  setTimeout(() => console.log("  B: setTimeout(0) inside I/O"), 0);
  setImmediate(() => console.log("  C: setImmediate inside I/O"));
  process.nextTick(() => console.log("  D: nextTick inside I/O"));
  Promise.resolve().then(() => console.log("  E: promise .then inside I/O"));
});
Promise.resolve().then(() => console.log("4: promise .then"));
process.nextTick(() => console.log("3: process.nextTick"));
console.log("2: script end");

// Actual output:
// 1: script start
// 2: script end
// 3: process.nextTick
// 4: promise .then
// 6: setTimeout(0)
// 7: setImmediate
// A: fs.readFile callback (poll phase)
//   D: nextTick inside I/O
//   E: promise .then inside I/O
//   C: setImmediate inside I/O   <- C before B is GUARANTEED inside an I/O callback
//   B: setTimeout(0) inside I/O

// Starvation test: recursive nextTick vs recursive setImmediate, racing an I/O callback
let count = 0;
fs.readFile(__filename, () => console.log("I/O fired after", count, "iterations"));
function recurseNextTick() {
  count++;
  if (count >= 200000) { console.log("capped, ioFired=false — nextTick starved the I/O callback entirely"); return; }
  process.nextTick(recurseNextTick);
}
// Swap for setImmediate(recurseNextTick) and the I/O callback fires after only ~6 iterations instead.
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 36 of 152 decoded in the Node.js track. One more won't hurt.

Back to track