Appearance
3.8.1 — Node.js: Architecture & the Event Loop's Phases
JavaScript was designed to run in a browser. In 2009 Ryan Dahl took Google's V8 engine (3.6.9), bolted on the ability to touch files, networks, and the operating system, and created Node.js — putting JavaScript on the server. His motivation explains Node's entire design: he had watched web servers waste enormous resources on the thread-per-connection model, where thousands of threads sat blocked doing nothing but waiting on I/O (2.7's C10k problem). JavaScript, with its single-threaded event loop and callback culture, was accidentally the perfect language for the alternative.
This folder opens Node completely — architecture, thread pool, buffers, streams, events, scaling, and the module zoo. This first page covers what Node is as a piece of software and the event loop's phases: the Node-specific refinement of 3.6.8's universal loop, home of the setTimeout-vs-setImmediate-vs-nextTick questions that appear in nearly every Node interview.
1. What Node.js actually is
Node is not a language and not a framework. It is a runtime: a program that embeds the V8 JavaScript engine and surrounds it with capabilities V8 alone doesn't have. V8 can evaluate JavaScript, but it knows nothing of files, sockets, or processes — in a browser those come from Web APIs; on a server they must come from somewhere else. Node supplies them.
Its architecture has three layers:
- V8 — parses, compiles, and executes your JavaScript (3.6.9: Ignition, TurboFan, Orinoco GC).
- libuv — a C library providing the event loop, asynchronous I/O across platforms, and a thread pool (3.8.2). This is where Node's concurrency actually lives.
- Node's own bindings and standard library — the C++ glue exposing operating-system capabilities to JavaScript, plus the built-in modules (
fs,http,crypto, …).
So when your code calls fs.readFile, the path is: JavaScript → Node's fs module → C++ binding → libuv → the operating system (2.1's syscall) → and back, the result delivered as a callback via the event loop. Everything in 3.6.8 applies here — Node simply supplies a different set of "platform APIs" than a browser does. The truth about "single-threaded" — your JavaScript is; Node is not — and exactly which work runs on which threads is the next page's subject (3.8.2).
2. The event loop's phases
3.6.8 gave the universal rule — drain all microtasks, then take a task. Node's libuv loop refines "task" into phases, each with its own queue, processed in fixed order every iteration (a "tick"):
process.nextTick first, then promises.- timers — callbacks for
setTimeout/setIntervalwhose time has elapsed. - pending callbacks — certain deferred system callbacks (e.g. some TCP errors).
- idle/prepare — internal use only (omitted from the figure).
- poll — the heart: retrieve new I/O events and run their callbacks (incoming requests, completed file reads). If nothing is pending, the loop may block here waiting for I/O — exactly the efficient sleeping of 2.7's
epoll_wait. - check — callbacks scheduled by
setImmediate. - close callbacks — e.g. a socket's
'close'event.
Between every phase (and between individual callbacks), Node drains the microtask queues — and Node has two, in priority order: first process.nextTick()'s queue, then the promise microtask queue. Full priority: process.nextTick → promises → the current phase's callbacks.
3. The classic ordering questions, settled
setTimeout(fn, 0) vs setImmediate(fn). setTimeout runs in the timers phase; setImmediate in the check phase. From the main module, their relative order is genuinely non-deterministic — it depends on whether the ~1 ms timer threshold elapsed during process startup, so the order can flip between runs. But inside an I/O callback (the poll phase), the order is guaranteed: setImmediate always fires first, because check comes immediately after poll, whereas timers wait for the next full iteration. ⚑Difference between setTimeout(fn, 0) and setImmediate()? [EQ-21]
process.nextTick() vs setImmediate(). Despite the names suggesting the opposite, process.nextTick fires sooner: it isn't a phase at all but a microtask-like queue drained between operations, before promises and before the loop advances. setImmediate is a proper phase callback. So nextTick runs before everything — powerful and dangerous: recursive nextTick starves the loop entirely (it never reaches poll; no I/O is ever processed). Use setImmediate to yield to the loop; use nextTick sparingly, for "run this right after the current operation, before anything else" (its legitimate niche: making an API consistently asynchronous — emitting an event after the caller has had a chance to attach listeners). ⚑What is the difference between process.nextTick() and setImmediate()? [EQ-20]⚑Explain the phases of the Event Loop. [EQ-8]
One fully-worked trace to lock the model — inside an I/O callback:
javascript
fs.readFile("data.txt", () => { // we are in the POLL phase
setTimeout(() => console.log("timeout"), 0); // → timers, NEXT iteration
setImmediate(() => console.log("immediate")); // → check, THIS iteration
process.nextTick(() => console.log("nextTick")); // → before anything advances
Promise.resolve().then(() => console.log("promise")); // → after nextTick
console.log("sync");
});
// Output, guaranteed: sync → nextTick → promise → immediate → timeoutRead it off the machinery: synchronous code finishes; the nextTick queue drains; the promise queue drains; the loop advances from poll to check (immediate); timeout waits for the next tick's timers phase.
4. The expert lens
Node's architecture is a single bet: optimize for waiting, not computing. Every design choice follows — the event loop, non-blocking I/O, streams, the callback/promise culture. It's excellent for the majority of backend work (APIs, gateways, proxies, real-time services), where the process spends its life waiting on databases and networks, and it lets one process handle connection counts that would need thousands of threads elsewhere (2.7). It's a poor fit for sustained CPU-heavy work unless you deliberately move that work off the loop (3.8.6). "Is Node fast?" — honestly: extremely efficient at concurrency-under-waiting, unremarkable at raw computation; knowing which your workload is decides whether it's the right tool.
Phases are the debugging map, not trivia. "Why does my timer fire late?" (a long poll callback or microtask storm delayed the timers phase), "why does this setImmediate beat my setTimeout?" (you're in an I/O callback), "why did my recursive queue-drainer freeze the server?" (nextTick starvation) — each everyday mystery resolves by placing the callback in Figure 2. When ordering matters and you find yourself unsure, don't guess and don't cargo-cult — trace the tick as in section 3's example. And when ordering matters architecturally, prefer explicit sequencing (promises, queues) over phase arcana: code whose correctness depends on setImmediate-vs-setTimeout subtleties is fragile by construction.
Next: the threads behind the curtain — 3.8.2: what's genuinely non-blocking, what secretly runs on 4 threads, and the production symptom that confusion causes.
Recall
- Node.js is a runtime: V8 (executes JavaScript) + libuv (event loop, async I/O, thread pool) + C++ bindings and standard library. Born to escape thread-per-connection waste (the C10k problem).
fs.readFile= JS → binding → libuv → syscall → callback via the loop. - Phases per tick: timers → pending callbacks → poll (I/O heart; may sleep) → check (
setImmediate) → close. Between every phase and callback:process.nextTickqueue, then promises — Node has two microtask queues. setTimeout(0)vssetImmediate: non-deterministic from the main module, butsetImmediatealways wins inside an I/O callback (check follows poll). process.nextTick runs before everything — recursion starves the loop; its niche is deferring emission until listeners attach. Guaranteed I/O-callback order: sync → nextTick → promise → immediate → next-tick timers.- The bet: optimize for waiting, not computing — superb for I/O-bound services, wrong for sustained CPU unless offloaded (3.8.6).
Self-test: Name Node's three layers and each one's job. Walk the phases of one tick and say where the process sleeps. Why is setTimeout(0) vs setImmediate non-deterministic at startup but deterministic in an I/O callback? Reproduce section 3's five-line output from the machinery. What is nextTick's legitimate use, and its failure mode?
Quiz Bank
FoundationalWhat is Node.js, architecturally?
Node.js is a runtime — not a language or framework — that embeds Google's V8 engine and surrounds it with capabilities V8 lacks. Three layers: V8 parses/compiles/executes JavaScript (3.6.9); libuv, a C library, provides the event loop, cross-platform asynchronous I/O, and the thread pool; and Node's C++ bindings + standard library expose OS capabilities (files, sockets, processes) to JavaScript. fs.readFile travels JavaScript → fs module → C++ binding → libuv → OS syscall (2.1) → back as a callback via the loop. Node was created to escape the thread-per-connection model, whose thousands of threads sat blocked waiting on I/O.
AppliedExplain the phases of Node's event loop.
Each tick processes fixed-order phases, each with its own queue: (1) timers — expired setTimeout/setInterval callbacks; (2) pending callbacks — certain deferred system callbacks; (3) idle/prepare — internal; (4) poll — the heart: collect new I/O events, run their callbacks, and if nothing is pending block here awaiting I/O (the efficient sleep of 2.7's epoll_wait); (5) check — setImmediate callbacks; (6) close callbacks (socket 'close'). Between every phase and between individual callbacks, Node drains its two microtask queues in priority order — process.nextTick first, then promises — giving the overall priority: nextTick → promises → current phase's callbacks.
InterviewWhat's the difference between setTimeout(fn, 0), setImmediate(fn), and process.nextTick(fn)?
Different homes. setTimeout(fn, 0) queues in the timers phase; setImmediate in the check phase (right after poll); process.nextTick isn't a phase — it's a microtask-style queue drained between operations, before promises and before the loop advances, so it runs soonest despite its name. Ordering: from the main module, setTimeout(0) vs setImmediate is non-deterministic (depends on whether the ~1 ms threshold elapsed during startup); inside an I/O callback, setImmediate is guaranteed first (check follows poll; timers wait a full iteration). Hazard: recursive nextTick starves the loop — no I/O ever runs; yield with setImmediate instead. Legitimate nextTick niche: keeping APIs consistently async — emit after the caller could attach listeners.
InterviewWhy is the poll phase called the heart of the loop, and what does it mean that Node sleeps there?
The poll phase is where Node meets the operating system: it retrieves completed I/O events (readable sockets, finished file reads via the pool — 3.8.2) and runs their callbacks — which in a server is nearly all the work. When no timers are due and no callbacks are queued, the loop doesn't spin — it blocks in the kernel (epoll_wait/kqueue/IOCP — 2.7) until an event arrives or the nearest timer's deadline approaches. That sleep is why an idle Node server consumes ~0% CPU while holding tens of thousands of open connections: the kernel does the waiting, and the process wakes only when there's actual work. It's the C10k answer in one mechanism — and it's also why blocking the loop is so harmful (3.8.2): while your code runs, nothing returns to poll, so all those ready events wait.
StaffA metrics daemon's setInterval(fn, 1000) drifts badly under load — ticks arrive late and sometimes bunch up. Explain from the loop's mechanics, and design timing that meets its guarantees honestly.
setInterval promises only "not before" — the callback becomes eligible in the timers phase, and the timers phase runs when the loop gets there. Under load, long poll-phase callbacks, microtask storms (each phase boundary drains them fully), or thread-pool-delayed I/O callbacks occupy the loop past the deadline; the tick fires late, and if multiple intervals elapse, callbacks bunch. Diagnose with perf_hooks.monitorEventLoopDelay() — loop lag is the drift, and it names the real culprit (blocked loop) rather than the symptom (late timer).
Design: (1) don't accumulate error — schedule each next run from the intended next deadline (next += 1000; setTimeout(fn, next - Date.now())) so drift self-corrects instead of compounding; (2) timestamp measurements with Date.now()/hrtime at execution and record the actual time, never assume the tick was on time — downstream math then survives late ticks; (3) attack the cause: find and fix whatever blocks the loop (chunk CPU work, offload to workers — 3.8.6); (4) if hard timing matters more than Node convenience, move the ticker to a worker thread with little else to do, or accept the platform's truth — an event loop is a cooperative scheduler, and cooperative schedulers give best-effort timing by construction (2.3's preemption contrast). Alert on loop lag, not on late metrics — it's the leading indicator.
Flashcards
FlashNode = ?
A runtime: V8 (runs JS) + libuv (event loop, async I/O, thread pool) + C++ bindings and standard library.
FlashEvent loop phases (Node)
timers → pending callbacks → idle/prepare → poll (I/O; may sleep) → check (setImmediate) → close callbacks. Microtasks drain between phases.
FlashNode's TWO microtask queues
process.nextTick queue first, then promises — both drained between every phase and callback.
FlashnextTick vs setImmediate
nextTick: between operations, before promises — soonest; recursion starves the loop. setImmediate: the check phase, right after poll.
FlashsetTimeout(0) vs setImmediate ordering
Non-deterministic from the main module; setImmediate guaranteed first inside an I/O callback (check follows poll).
FlashThe I/O-callback order
sync → nextTick → promises → setImmediate (check) → setTimeout (next tick's timers).
Scenario Drill
DrillA teammate's queue-drainer uses process.nextTick recursively to process items 'as fast as possible', and under a large batch the health-check endpoint stops responding entirely, though CPU shows one busy core. Explain the freeze mechanically and redesign the drainer.
The freeze is nextTick starvation, visible straight from the phase model. process.nextTick callbacks are drained completely, between operations, before the loop advances — and each drained callback that schedules another nextTick appends to the same queue currently being drained. A recursive drainer therefore never lets the loop reach the poll phase: incoming health-check requests sit as ready events the loop never retrieves, timers never fire, and the process appears hung while one core burns (the drain loop is pure JavaScript execution). It's the cooperative-scheduling failure in miniature: a task that never yields owns the machine (2.3).
Redesign: (1) swap the recursion to setImmediate — a check-phase callback: each item processed schedules the next via setImmediate, so between items the loop completes its tick — polling I/O, serving health checks, firing timers — and the batch still progresses at high speed (this is the standard "yielding loop" idiom); (2) better, process in chunks per yield (drain up to N items or T milliseconds, then setImmediate the continuation) to amortize scheduling overhead while bounding loop occupancy — tune N/T against monitorEventLoopDelay; (3) if items are CPU-heavy rather than merely numerous, chunking still occupies the loop — move the work to worker_threads (3.8.6) and keep the loop for I/O; (4) add a loop-lag alert so the next starvation regression is a dashboard spike, not an outage. Rule to leave the team: nextTick runs before the world and must never recurse; setImmediate is the polite "continue after the loop breathes" — pick by whether you intend to preempt the loop or yield to it.