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.nextTickqueue- 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:
- Execute current JS
- Drain
process.nextTickqueue completely - Drain Promise microtask queue completely
- 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.nextTickis 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?
nextTickruns first (highest priority)- Promise microtasks run next
- 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.nextTickhas higher priority than Promises- Both are higher priority than the event loop
nextTickis 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