Event loop phases (libuv): timers, poll, check, close
The loop is a fixed round of phases; what runs next depends on which phase you are in now, not on a delay of 0.
Predict first
Before you read: predict
ExamHall's autosave server receives each draft on a socket. Its 'data' handler is below; validate is synchronous and takes 20 ms.
In what order do the three log lines print?
socket.on('data', (draft) => { setTimeout(() => log('flush'), 0); setImmediate(() => { log('ack'); setImmediate(() => log('receipt')); }); validate(draft);});The 30-second version
Node runs callbacks on one thread while libuv cycles: timers, pending, poll, check, close. Poll runs I/O callbacks and waits for I/O unless an immediate is queued. Check runs immediates queued before it began; newer ones wait a turn. A timer runs once its delay, at least 1 ms, has passed. After I/O, immediates beat setTimeout(0).
The loop is a fixed round of phases; what runs next depends on which phase you are in now, not on a delay of 0.
How it works
Step 1 of 12
Analogy: A circular bus route
One bus runs one loop of fixed stops, always in the same order.
| On the route | In Node |
|---|---|
| the clock-tower stop, where "pick me up at 10:05 or later" bookings board on the first lap after 10:05 | timers (step 3) |
| the depot stop, collecting what was left over last lap | pending (step 4) |
| the interchange, where the driver waits for passengers, but not past the next booked pickup, and not at all if someone waits at the next stop | poll (steps 5–6) |
| the stop right after the interchange; someone who books it while the bus stands there rides next lap | check (step 7) |
| the log of stops closed during the lap | close (step 8) |
| the route runs while anyone holds a booking; a "don't run the bus just for me" booking does not count | staying alive, unref (step 11) |
In real terms: to predict an order, ask which phase is running now and which phase comes next; a delay of 0 only sets a 1 ms threshold.
Breaks down when: a driver can see a passenger running for the bus and wait. A phase never waits for a callback that is not queued yet; one queued mid-phase waits a round (step 7).
Contrasting case: the browser has no check phase and no setImmediate; its tasks and microtasks are the prerequisite's model.
Examples, run for real
4.1 ExamHall: "Saved" before the draft was saved
EXH-1310: the autosave server's socket handler flushed the draft from setTimeout(…, 0) and sent the "saved" acknowledgement from setImmediate. A unit test ran the same two calls in the main module and saw the flush first. In production, candidates saw "saved" before the draft reached disk. MR !1510 sends the acknowledgement from the write's own callback.
4.1 as shipped, in the socket's data callback: ack "saved" sent → draft on disk
4.1 MR !1510, ack from the write's callback: draft on disk → ack "saved" sent
The ungraded measurement (race-split.mjs, 50 fresh processes per line) shows why the test misled:
main module first: timeout 7, immediate 43 (50 runs)
inside a setImmediate callback first: timeout 5, immediate 45 (50 runs)
inside an fs.readFile callback first: timeout 0, immediate 50 (50 runs)
In the main module the order varies (step 10); inside an I/O callback it never does (step 9). The split itself changes between machines and runs. Sequencing by dependency, not by phase, makes the order part of the code.
4.2 Chat: 100 open sockets, one thread
A chat server holds 100 connections. For 500 ms nobody talks; then each client sends one message.
4.2 then each sends one message: handled 100 of 100 · every callback on the main thread: true
While the sockets are quiet, poll waits on all of them at once (step 12). When the messages come, every callback runs on the one main thread, one after another.
Picture it

Try it
Answer first, then check. Nothing is saved.
Question 1 of 4
A catalog API process is idle: no requests in flight, one setTimeout(sendDigest, 2000) pending, nothing else queued. A shopper's socket is open, and about 300 ms later its request arrives.
What happens?
Question 2 of 4
A nightly settlement script starts a watchdog, setTimeout(warnStillRunning, 600_000), then runs the settlement. The settlement finishes in about a minute, but the process stays alive until the 10-minute mark and then prints a false "still running" warning.
Which one change makes it exit as soon as the settlement finishes, while a settlement that really runs past 10 minutes still gets the warning?
Question 3 of 4
A checkout server's socket 'data' handler runs a fan's purchase as three steps: setImmediate(reserve), setImmediate(charge), setImmediate(confirm). reserve takes a while. A fan who taps cancel while the seat is being reserved is still charged: the server handles the cancel only after confirm.
Which change lets a cancel sent during reserve stop the charge?
Question 4 of 4
A streaming recorder waits for a go-live message on its control socket. The socket's 'data' handler sets live = true. A check function starts recording if live is true, and otherwise calls setImmediate(check). Recording starts on time, but while it waits, the idle recorder holds one CPU core at 100%.
Which is the bug?
Go deeper
Pro tips
- Never test an ordering in the main module. If a test must pin
setImmediateagainst a timer, schedule both from an I/O callback, or better, chain them. When: unit tests of scheduling code (steps 9–10). Verify (RAN,race-split.mjs, 50 runs per line): the main module split 7/43; inside anfscallback, 0/50. - Use
unref()for watchdogs and metrics timers. They then never hold a finished process open. When: CLI tools, scripts and shutdown paths (step 11).
Source
- Node.js, The Node.js Event Loop: https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick · fetched 2026-09-28. The phase diagram (timers, pending callbacks, idle/prepare, poll, check, close callbacks; step 1). A timer "specifies the threshold after which a provided callback may be executed" (step 3). Pending runs "callbacks for some system operations such as types of TCP errors" (step 4). Poll: "If scripts have been scheduled by
setImmediate(), the event loop will end the poll phase and continue to the check phase"; otherwise it "will wait for callbacks to be added to the queue" (steps 5–6). Close: "If a socket or handle is closed abruptly (e.g.socket.destroy()), the 'close' event will be emitted in this phase. Otherwise it will be emitted viaprocess.nextTick()" (step 8). "if you move the two calls within an I/O cycle, the immediate callback is always executed first"; from the main module "timing will be bound by the performance of the process" (steps 9–10). "Starting with libuv 1.45.0 (Node.js 20), timers are run after the poll phase in each event loop iteration … libuv 1.45.0 still runs timers once before entering the event loop." Valid as of 2026-09-28. - libuv, Design overview: https://docs.libuv.org/en/v1.x/design.html · fetched 2026-09-28. The iteration: due timers (for
UV_RUN_DEFAULT), pending, idle, prepare, poll with a computed timeout, check, close callbacks, "The loop concept of 'now' is updated", due timers again (steps 1, 7). The poll timeout is 0 when "there are any idle handles active" or "any handles pending to be closed", else the closest timer's, else infinite (step 6; Node keeps an idle handle active while immediates are queued). "If a loop has active and ref’d handles, active requests or closing handles it’s considered to be alive" (step 11). Valid as of 2026-09-28. - Node.js v24 API, Timers: https://nodejs.org/docs/latest-v24.x/api/timers.html · fetched 2026-09-28.
setImmediate: "The entire callback queue is processed every event loop iteration. If an immediate timer is queued from inside an executing callback, that timer will not be triggered until the next event loop iteration" (step 7).setTimeout: adelay"less than 1 … will be set to 1" (step 3); Node "makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering" (so no key here orders two timers against each other).timeout.unref(): "the process may exit before the Timeout object's callback is invoked" (step 11). Valid as of 2026-09-28.
Related lessons
Checked against the tools and docs as of 2026-09-28.