The Hidden Priority System in Node.js: Why nextTick Can Starve Your Entire App

The Hidden Priority System in Node.js: Why nextTick Can Starve Your Entire App

Most engineers think Node.js executes callbacks in a simple event loop.

It doesn’t.

There’s a hidden priority system — one that can override the event loop entirely, reorder execution, and even starve I/O without you realizing it.

If you’ve ever seen:

  • delayed network responses
  • unexpected execution order
  • CPU spikes with “async” code

You’ve probably hit this system.


The Problem

Node.js is often explained like this:

“Async work completes → callback goes into event loop → event loop executes it.”

That’s incomplete.

In reality, Node.js has multiple queues with strict priority rules:

  • process.nextTick queue
  • Promise microtask queue
  • Event loop queues (timers, poll, check, etc.)

And the ordering is not intuitive:

process.nextTick > Promise microtasks > Event loop

This means some callbacks can run before the event loop even gets a chance to continue.


Why It Happens

Node.js uses two distinct scheduling layers:

1. Microtask Queues (High Priority)

  • process.nextTick
  • Promise .then(), .catch(), .finally()

2. Event Loop (Lower Priority)

  • timers (setTimeout)
  • I/O callbacks
  • setImmediate

After every single operation, Node.js does this:

  1. Execute current JS
  2. Drain process.nextTick queue completely
  3. Drain Promise microtask queue completely
  4. Continue event loop

This behavior is recursive.

If microtasks keep adding more microtasks → Node.js keeps executing them → event loop never resumes


Internal Architecture

Node.js internally maintains two separate microtask queues:

  • nextTick queue (Node-specific, highest priority)
  • Promise queue (standard microtasks)

Execution order:

flowchart TD
    A[Current JS Execution] --> B[nextTick Queue]
    B --> C[Promise Microtask Queue]
    C --> D[Event Loop Phases]
    D --> A

Important detail:

process.nextTick is NOT part of the event loop.

It runs between phases, acting like a “priority interrupt”.


Example Scenario

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

setImmediate(() => console.log("immediate"));

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

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

Output:

nextTick
promise
timeout / immediate (depends)

Why?

  1. nextTick runs first (highest priority)
  2. Promise microtasks run next
  3. Only then event loop phases execute

The Real Danger: Starvation

nextTick Starvation

function loop() {
  process.nextTick(loop);
}

loop();

This will:

  • NEVER return control to event loop
  • NEVER process I/O
  • Freeze your system

Why?

Because Node keeps draining nextTick queue before doing anything else


Promise Starvation

function loop() {
  Promise.resolve().then(loop);
}

loop();

Less aggressive, but still dangerous:

  • Promises run after nextTick
  • But still before event loop
  • Can delay timers and I/O significantly

Diagram

Full Scheduling Model

flowchart LR
    JS[JS Execution] --> NT[nextTick Queue]
    NT --> PM[Promise Microtasks]
    PM --> EL[Event Loop]

    EL --> T[timers]
    EL --> P[poll]
    EL --> C[check]

    C --> JS

Starvation Flow

flowchart TD
    A[Execute nextTick] --> B[Adds nextTick]
    B --> A
    A --> C[Event Loop]

    style C fill:#ffcccc

Event loop is never reached.


Key Takeaways

  • process.nextTick has higher priority than Promises
  • Both are higher priority than the event loop
  • nextTick is not part of event loop
  • Microtasks are drained completely before moving on
  • Recursive microtasks can starve I/O and freeze systems
  • Promise chains can silently delay real work

Practical Debugging Tips

1. Watch for recursive microtasks

If CPU is high but I/O is low → suspect this

2. Avoid heavy logic in nextTick

Use it only for:

  • error handling
  • deferring small work

3. Prefer setImmediate for yielding

setImmediate(() => {
  // allows event loop to continue
});

4. Break long Promise chains

Bad:

Promise.resolve().then(a).then(b).then(c)...

Better:

setImmediate(() => runNextStep());

Final Thoughts

The Node.js event loop is not just a loop.

It’s a priority scheduler with hidden preemption layers.

And if you don’t understand microtasks:

  • your mental model is incomplete
  • your performance debugging will be misleading
  • your system can fail in subtle ways

Senior engineers don’t just know the event loop.

They understand what runs before the event loop even begins.


Next in the Series

Continue reading: How to Freeze 10,000 Users With One Line of Code in Node.js