Event Loop, libuv and the Thread Pool
Know the libuv phases, how the nextTick and microtask queues are drained between operations, which work goes to the thread pool, and how to spot and fix a blocked loop.
Key points
- 1
Each iteration runs timers → pending callbacks → idle/prepare → poll → check (
setImmediate) → close callbacks; the poll phase blocks for I/O when nothing else is pending. - 2
After every callback, Node drains the nextTick queue, then the promise microtask queue, and repeats until both are empty (per callback since Node 11).
- 3
In CommonJS the main script's nextTicks run before its promise callbacks; in ES modules the order flips, because module code runs inside the microtask checkpoint.
- 4
Inside an I/O callback
setImmediatealways beatssetTimeout(fn, 0); in the main module their order is nondeterministic. Delays below 1 ms or above 2^31 − 1 ms become 1 ms. - 5
fs,
dns.lookup, async crypto and zlib share libuv's thread pool (4 threads by default,UV_THREADPOOL_SIZE); sockets anddns.resolve*don't use it. - 6
Sync crypto/zlib, huge
JSON.parse, pathological regexes and recursive nextTick or promise loops block everything; measure withmonitorEventLoopDelayandeventLoopUtilization. - 7
The process stays alive while referenced handles exist (servers, sockets, ref'd timers); pending promises don't count, and
unref()opts a handle out.
Common traps
Recursing with
process.nextTickorawait Promise.resolve()doesn't yield to I/O; usesetImmediateto split work.Raising
UV_THREADPOOL_SIZEdoes nothing for*SyncAPIs, which always run on the main thread.Listeners called by
emit()run in the emitter's async context, so AsyncLocalStorage values can leak between requests unless you useAsyncResource.bind.
Read the source
Test yourself on Event Loop, libuv and the Thread Pool
Ten questions, with the answer and explanation after each one.