Skip to content

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?

Code for this question
socket.on('data', (draft) => {  setTimeout(() => log('flush'), 0);  setImmediate(() => {    log('ack');    setImmediate(() => log('receipt'));  });  validate(draft);});
Pick one

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

One thread, one round. Your JavaScript runs on one thread. libuv repeats a round of phases: timers, pending, poll, check, close, then timers again.

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

a dark-haired man in a collared green shirt and blue trousers walks left to right along a teal floor strip. He passes a low wooden cabinet with an hourglass on top, a teal counter with one wrapped parcel, a metal tray on the floor holding a folded pale cloth, and, at the right end, a tall green two-door cabinet. There is no text. The right quarter of the canvas is empty.
An illustration to help it stick. The mechanism is in the sections above.

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?

Pick one

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?

Pick one

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?

Pick one

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?

Pick one

Go deeper

Pro tips
  1. Never test an ordering in the main module. If a test must pin setImmediate against 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 an fs callback, 0/50.
  2. 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 via process.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: a delay "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.

Checked against the tools and docs as of 2026-09-28.