Glossary / Node.js event loop
Node.js event loop
The Node.js event loop coordinates JavaScript callbacks with operating-system readiness and libuv work so a mostly single JavaScript thread can handle many concurrent I/O operations. Understanding its phases, microtask checkpoints, and blocking limits helps explain ordering, latency, and why “async” code can still stall a server.
Phases and scheduling
The loop cycles through phases including timers, pending callbacks, idle/prepare, poll, check, and close callbacks. The poll phase handles many I/O callbacks and may wait for work; setImmediate callbacks run in the check phase. Timers become eligible after a threshold, but are not exact deadlines. Since Node.js 20/libuv 1.45, timers run after poll rather than both before and after it, which can affect some setTimeout(0) versus setImmediate observations. Relative ordering can depend on where scheduling occurs and runtime version.
Node also drains the process.nextTick() queue and promise microtasks at defined checkpoints. nextTick is not simply another libuv phase; recursively scheduling it can starve I/O. Promise callbacks and queueMicrotask also run before the loop advances to another phase, and unbounded microtask production can starve timers and I/O.
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('check'));
At top level, do not rely on a fixed relative order across environments; inside an I/O callback, setImmediate normally runs before a newly scheduled zero-delay timer.
What runs off the JS thread
Many filesystem, DNS, crypto, and compression operations use libuv's worker pool; network sockets are generally readiness-driven. JavaScript callbacks still execute on the event-loop thread unless code is explicitly moved to a worker thread or child process. Synchronous CPU work, large JSON parsing, synchronous filesystem calls, and catastrophic regular expressions block that thread and delay all clients sharing it.
Engineering guidance
Measure event-loop delay and callback duration. Break up long cooperative CPU work, use worker threads for CPU-bound parallelism, and size libuv's thread pool with awareness that it is shared by certain APIs. Do not use setTimeout(0) as a precise yield guarantee. Pin Node version when depending on ordering, and test scheduling assumptions at the actual scheduling site.
Interview questions
- Is
setTimeout(fn, 0)immediate? No; it schedules eligibility after a threshold, and other work may run first. - How does
setImmediatediffer? It runs in the check phase, commonly after poll callbacks. - Why can
process.nextTickcause starvation? Its queue is drained before the loop proceeds, so recursive scheduling can prevent I/O phases. - Does async filesystem I/O execute JavaScript in parallel? The underlying work may use libuv's pool, but the JS callback runs on the event-loop thread.
- What changed in Node 20? libuv changed timer processing to run timers after poll, affecting certain ordering observations.