Inside the Node.js Event Loop
How does the Node.js event loop work, and how can Node handle many concurrent I/O operations if JavaScript runs on one thread?
what they're testing: The interviewer is testing whether you can reason about asynchronous execution, callback ordering, and the performance cost of blocking shared runtime resources.
Node handles many concurrent I/O operations by keeping the event-loop thread free to run JavaScript callbacks one at a time. Network sockets use non-blocking operating-system polling. File-system work, dns.lookup(), and selected crypto and compression APIs use libuv's worker pool. When an operation is ready or complete, its callback can be scheduled back on the event loop.
The loop processes work in phases. The main phases handle timers, pending callbacks, I/O polling, setImmediate() callbacks, and close callbacks. Each phase has its own queue and rules. After a JavaScript operation finishes, Node drains the process.nextTick() queue before continuing the loop, with the promise microtask queue drained immediately afterward. Initial ES module evaluation is a special case because it already runs as a microtask.
This model provides I/O concurrency, but JavaScript on one event loop does not run in parallel. A long callback, synchronous file-system call, or CPU-heavy loop blocks that event loop and delays other clients. Timer delays are minimum thresholds, so setTimeout(fn, 10) makes fn eligible after 10 milliseconds but does not guarantee that start time. Partition CPU-heavy work so it yields, or move it to a reusable worker-thread pool or another process.
Where people slip
the tempting wrong answer, and what's actually true
Node.js is completely single-threaded, so all I/O runs on the JavaScript thread.
JavaScript callbacks run on one event-loop thread by default, but network readiness is polled through the operating system and some APIs use libuv's worker pool.
The event loop is one FIFO queue of every callback.
The event loop has distinct phases with phase-specific callback queues and scheduling rules.
A zero-delay timer runs immediately after the current function.
A timer delay is a minimum threshold. A zero-delay callback becomes eligible during timer processing and may be delayed by other work.
Making a function async prevents CPU-heavy code inside it from blocking Node.
CPU-heavy JavaScript still blocks the event-loop thread unless the work yields or is moved to a worker thread or process.
If they push further
What is the difference between `setImmediate()` and `setTimeout(fn, 0)`?
`setImmediate()` runs in the check phase after poll, while a timeout runs during timer processing once its threshold has passed; outside an I/O cycle, their relative order is not guaranteed.
Where do promises and `process.nextTick()` fit?
After the current JavaScript operation, Node drains the next-tick queue and then the promise microtask queue before continuing the loop. Initial ES module evaluation can show the opposite order because it already runs in the microtask queue. Recursive scheduling can starve I/O.
How would you handle CPU-intensive work?
Keep the event-loop callbacks short, then partition the work so it yields or move it to a reusable `worker_threads` pool; workers are intended for CPU-heavy JavaScript rather than ordinary asynchronous I/O.
Sources
- Node.js: The Node.js Event Loop ↗nodejs.org
- Node.js: Don't Block the Event Loop (or the Worker Pool) ↗nodejs.org
- Node.js: Process API ↗nodejs.org
- libuv: Design Overview ↗docs.libuv.org
- Node.js: Worker Threads ↗nodejs.org
Now answer it yourself.
Reading a strong answer is easy. Producing one under pressure is the skill the interview tests. Gapmap grades your answer against the same bar an interviewer would.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.