libuv Phases
The libuv event loop runs phases in a fixed order each tick - knowing the sequence explains timer drift, setImmediate behavior, and when I/O callbacks fire.
Search across all documentation pages
The libuv event loop runs phases in a fixed order each tick - knowing the sequence explains timer drift, setImmediate behavior, and when I/O callbacks fire.
Phase order (one loop iteration):
timers → pending → idle/prepare → poll → check → close callbacks
↑ microtasks drain between phases ↑
import { setTimeout, setImmediate } from 'node:timers';
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));When to reach for this:
setImmediate vs setTimeout(0) orderingdata handlers run relative to timersimport { readFile } from 'node:fs';
import { setTimeout, setImmediate } from 'node:timers';
readFile(__filename, () => {
console.log('1: I/O callback (poll phase)');
setTimeout(() => console.log('2: timer'), 0);
setImmediate(() => console.log('3: immediate (check phase)'));
});
setTimeout(() => console.log('4: outer timer'), 0);
setImmediate(() => console.log('5: outer immediate'));Typical output pattern:
readFile callback: 1, then 3 (immediate), then 2 (timer).What this demonstrates:
setImmediate in an I/O callback runs in the check phase of the same iterationsetTimeout, setInterval).setImmediate callbacks.close event callbacks (e.g., socket.on('close')).| Phase | API examples | Notes |
|---|---|---|
| timers | setTimeout, setInterval | Minimum delay, not exact under load |
| poll | fs.readFile, socket.on('data') | Can block if no other work scheduled |
| check | setImmediate | Runs after poll in same tick |
| close | server.close, handle cleanup | Last chance for resource teardown |
import { setImmediate } from 'node:timers';
// Defer work until after I/O callbacks in this tick
function deferAfterIo(fn: () => void): void {
setImmediate(fn);
}setTimeout(fn, 100) fires exactly at 100ms - phases and load add drift. Fix: use monotonic clocks for deadlines, not timer count alone.setImmediate loops - check phase never yields to I/O. Fix: use setImmediate once per item, batch work, or use workers.| Alternative | Use When | Don't Use When |
|---|---|---|
queueMicrotask | Run before next macrotask, after current stack | You need to wait for I/O poll to finish |
setImmediate | Defer after current poll phase | Sub-millisecond timing precision required |
setTimeout | Time-based scheduling with tolerance | Exact scheduling under heavy CPU load |
| Worker thread message | CPU work off main thread | Simple deferral of a few lines |
One full cycle through timers, pending, idle/prepare, poll, check, and close - with microtasks drained between phases.
In the check phase, immediately after the poll phase completes in that iteration.
Timers fire only when the loop reaches the timers phase. Blocking sync work or saturated poll delays that phase.
The socket data handler in poll phase, then microtasks from any Promises it creates, then check-phase immediates.
It can wait for I/O with a computed timeout. If timers or immediates are due, it times out and continues.
nextTick is not a libuv phase - it runs between phases and before Promise microtasks. Overuse starves I/O.
When tearing down servers and sockets - close callbacks release handles. Important for graceful shutdown.
No - libuv always walks the cycle. You influence what callbacks are queued for each phase.
Similar microtask/macrotask ideas; Node adds setImmediate, process.nextTick, and different I/O integration via libuv.
NODE_DEBUG=timer traces timer insertions and firings - useful locally, noisy in production.
Yes - network I/O completion callbacks schedule through libuv like node:http.
Each middleware await yields microtasks; response send triggers I/O in poll. Long sync middleware blocks all phases.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026