# Node's Event Loop (libuv) — Part 1: Mapping Node's APIs to the Phases

This topic assumes you've read `languages/javascript/27-event_loop` (`js_event_loop_theory_1.md` and `js_event_loop_theory_2.md`). That topic already covers the generic call-stack/queue mechanism, and its "Under the Hood" section already introduced libuv's **six phases by name** (timers → pending callbacks → idle/prepare → poll → check → close callbacks). This topic does not re-introduce that list — it goes further: **which specific Node APIs land in which phase, and why**, plus the thread pool's role from an architectural standpoint. Part 2 covers `process.nextTick`, `setImmediate` vs `setTimeout` ordering in depth, and `UV_THREADPOOL_SIZE` tuning.

## 1. The Phases Aren't Generic — Each One Is Backed By Specific Node Machinery

| Phase | What actually runs here | Node APIs |
|---|---|---|
| **timers** | callbacks whose scheduled time has passed, pulled from a min-heap of `(expiry, callback)` pairs | `setTimeout`, `setInterval` |
| **pending callbacks** | some system-level callbacks deferred from the previous loop iteration (e.g. certain TCP errors) | mostly internal |
| **idle, prepare** | internal libuv bookkeeping | none you call directly |
| **poll** | (a) retrieve new I/O events from the OS, (b) execute their callbacks; also where the loop **blocks and waits** if nothing else is scheduled | `fs.*` (threadpool results land here), `net`/`http` socket readable/writable events |
| **check** | `setImmediate` callbacks, specifically, always after poll | `setImmediate` |
| **close callbacks** | cleanup callbacks for things closed abruptly | `socket.on('close', ...)`, `server.close()` |

## 2. Two Fundamentally Different Roads Into the Poll Phase

The most important architectural fact this topic adds on top of the phase list: **`fs.readFile` and a `net` socket read both eventually surface their callback in the poll phase, but they get there through completely different mechanisms.**

```js
const fs = require("fs");
const net = require("net");

fs.readFile(__filename, () => console.log("fs callback — poll phase, via THREADPOOL"));

net.createServer((socket) => {
  socket.on("data", () => console.log("socket callback — poll phase, via OS EPOLL/KQUEUE"));
}); // (not started — illustrative)
```

- **`fs.readFile`**: Node hands the blocking `read()` syscall to a **libuv threadpool** worker thread. The worker blocks (that's fine — it's not the JS thread), and when it finishes, it posts a completion event back to the loop. The loop picks that up and runs your callback during the **poll** phase.
- **`net` socket data**: libuv registers the socket file descriptor with the OS's native async I/O facility (epoll on Linux, kqueue on macOS/BSD, IOCP on Windows) via a single call in the **poll** phase itself. No thread pool worker is spent per socket — the OS itself notifies libuv "this fd is readable," and libuv fires the callback directly. This is architecturally why Node can hold open tens of thousands of idle sockets cheaply: it costs one file descriptor and a kernel-level registration, not a thread.

![fs.readFile takes the threadpool road into the poll phase; a net socket read takes the OS epoll/kqueue road directly into the poll phase, without touching the threadpool](diagrams/01-two-roads-to-poll.svg)

## 3. `setImmediate` — Deliberately a Separate Phase From `timers`

```js
const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
  // inside an I/O callback, immediate ALWAYS logs first — deterministically
});
```

`setImmediate` exists specifically to run **"right after I/O, this same iteration,"** which is exactly what "check phase, right after poll" gives you. That's a different guarantee than `setTimeout(fn, 0)`, which lands in the **timers** phase — a phase the loop only reaches at the *top* of its next full cycle. When both are scheduled inside an I/O callback (as above), the loop is already past `timers` for this cycle and is sitting in `poll`; it proceeds to `check` next, so `setImmediate` reliably wins. At the top level of a script (outside any I/O callback), which one wins depends on how much time has elapsed reaching that point relative to the timer's expiry — which is why that specific case is the famous "non-deterministic" one (Part 2 covers this ordering question in more depth alongside `process.nextTick`).

## Common Pitfalls

1. **Assuming all async Node APIs work the same way** — `fs` and `net` both "feel" async from JS, but one consumes a threadpool thread and one doesn't; this matters for capacity planning under load (see Part 1's Architecture and Runtime topic for the threadpool-sizing implications).
2. **Expecting `setImmediate` and `setTimeout(fn, 0)` to have a fixed relative order everywhere** — it's deterministic inside an I/O callback, not at the top level of a script.
3. **Forgetting the poll phase can BLOCK** — if nothing else is scheduled, the loop waits in poll for I/O rather than spinning; this is *why* an idle Node process uses ~0% CPU instead of busy-looping.
4. **Confusing "pending callbacks" (a specific phase for deferred system callbacks) with "any pending async operation"** — it's a narrow, mostly-internal phase, not a general term.

## Quick Reference

```
fs.*            → threadpool → poll phase
net/http sockets → OS epoll/kqueue/IOCP → poll phase (no threadpool)
setTimeout/Interval → timers phase (min-heap by expiry)
setImmediate     → check phase (always right after poll)
socket 'close'   → close callbacks phase
```

Continue to `node_event_loop_libuv_theory_2.md` for `process.nextTick`, the full ordering algorithm across phases, and threadpool tuning.
