# Node's Event Loop (libuv) — Part 2: process.nextTick, Full Ordering, and Threadpool Tuning

Part 1 mapped Node's APIs onto libuv's six phases. This part covers the queue that ISN'T one of the six phases at all — `process.nextTick` — and pulls everything (phases, microtasks, `nextTick`) into one precise ordering model, then covers `UV_THREADPOOL_SIZE` tuning in practice.

## 1. `process.nextTick` Is Not Part of libuv

This is the single most commonly misunderstood fact about Node's loop: **`process.nextTick` has nothing to do with libuv's phases.** It's a Node-specific queue, drained by Node's C++ layer — not a libuv construct at all — and it runs **before Promise microtasks**, and after **every single callback**, not just once per phase.

```js
console.log("1: sync");

setTimeout(() => console.log("5: timeout"), 0);

process.nextTick(() => console.log("3: nextTick"));

Promise.resolve().then(() => console.log("4: promise microtask"));

console.log("2: sync");
```

Output: `1`, `2`, `3`, `4`, `5` — `nextTick` beats the Promise microtask, and both beat the macrotask-equivalent timer, exactly matching the generic microtask-drains-before-macrotask rule from `languages/javascript/27-event_loop`, but with `nextTick` given strict priority even over Promise callbacks.

## 2. The Complete, Precise Ordering

```
after EVERY callback that runs (not just once per phase, EVERY single one):
  1. drain process.nextTick queue COMPLETELY (including new nextTicks added while draining)
  2. drain the Promise microtask queue COMPLETELY (including new ones added while draining)
  3. proceed to the next scheduled callback (may be same phase, or move to next phase)
```

This nests inside the six-phase cycle from Part 1: every time the loop is about to move from one callback to the next — even within the *same* phase, not just at phase boundaries — it stops and fully drains `nextTick`, then microtasks, before continuing.

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

fs.readFile(__filename, () => {
  console.log("A: fs callback (poll phase)");
  process.nextTick(() => console.log("B: nextTick"));
  Promise.resolve().then(() => console.log("C: microtask"));
  setImmediate(() => console.log("D: setImmediate (check phase)"));
});
```

Output: `A`, `B`, `C`, `D` — after the poll-phase callback (`A`) finishes, `nextTick` (`B`) and microtasks (`C`) drain completely before the loop even considers moving to the check phase for `D`.

## 3. Why the Top-Level `setTimeout` vs `setImmediate` Race Is Genuinely Non-Deterministic

```js
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
```

At the top level of a script, both timers and check are still ahead of the loop when it starts. Which one the loop reaches "first" this cycle depends on exactly how much wall-clock time elapsed between process start and the loop's first pass through the timers phase — if more than ~1ms has already passed (process startup, module loading), the 0ms timer is already "expired" and fires in the timers phase before the loop even reaches check; if not, the timer isn't expired yet, the loop moves on to poll/check first, and `setImmediate` wins. This is real OS scheduling jitter, not a bug — which is exactly why Node's own docs call this specific ordering "not guaranteed" only at the top level.

## 4. Threadpool Sizing in Practice

```bash
UV_THREADPOOL_SIZE=8 node server.js
```

```js
// or, before ANY threadpool-using call in your entry file:
process.env.UV_THREADPOOL_SIZE = 8;
```

Raising the pool size gives more concurrent slots for `fs.*`, `dns.lookup`, and async `crypto` calls to run without queueing behind each other — useful for I/O-heavy workloads issuing many concurrent filesystem operations. It does **not** help:

- Network-socket-heavy workloads (`net`/`http` don't use the pool at all — see Part 1 §2).
- CPU-bound JavaScript (a tight loop in your own JS still fully occupies the one JS thread; more pool threads doesn't give your JS more places to run — see the Worker Threads topic for actual parallel JS execution).

```
UV_THREADPOOL_SIZE too small → fs-heavy code queues, throughput caps well below CPU capacity
UV_THREADPOOL_SIZE raised     → more concurrent fs/dns/crypto slots, up to the OS's own I/O limits
UV_THREADPOOL_SIZE raised, but bottleneck is CPU-bound JS → no improvement, wrong knob entirely
```

![nextTick and microtask queues drain completely between every callback, nested inside the six-phase libuv cycle, not just once per phase](diagrams/02-nexttick-and-phases.svg)

## Common Pitfalls

1. **Treating `process.nextTick` as "the same as" `setImmediate`** — the names invite confusion, but they're opposite ends of the priority spectrum: `nextTick` runs before even microtasks, `setImmediate` runs in the check phase, one of the *last* things each cycle.
2. **Recursively scheduling `process.nextTick`** can starve the entire event loop (no phase, no I/O, no timer ever runs) — the same starvation risk the generic microtask topic describes, but worse here because `nextTick` is drained with even higher priority than microtasks.
3. **Relying on `setTimeout(fn, 0)` vs `setImmediate()` ordering at the top level of a script** — only reliable inside an I/O callback (Part 1 §3); flaky at the top level.
4. **Raising `UV_THREADPOOL_SIZE` to "fix" a slow server** without profiling first — if the bottleneck is CPU-bound JS or network sockets, this env var changes nothing.

## Quick Reference

```
process.nextTick  → Node-specific queue, NOT libuv, drained before microtasks, after every callback
Promise microtasks → drained after nextTick, still after every callback
six libuv phases   → nested OUTSIDE the nextTick/microtask draining, not the other way around
setImmediate vs setTimeout(fn,0): deterministic inside I/O callbacks only (immediate wins)
UV_THREADPOOL_SIZE: helps fs/dns/crypto concurrency; does nothing for sockets or CPU-bound JS
```
