Module A-3·33 min read

The math behind event loop lag, microtask queue starvation, and UV_THREADPOOL_SIZE tuning for cryptographic validation at scale.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module 2 — Event Loop Saturation & Thread Pool Offloading

What this module covers: The event loop from Module 0 was presented as a single loop. That was a simplification. The event loop has six distinct phases, each with its own queue, processed in strict order. When you have thousands of incoming transactions per second, microtask queues competing with I/O callbacks, timers firing at sub-millisecond intervals, and cryptographic operations queuing for a thread pool that defaults to 4 threads — the phase structure determines everything about your latency profile. This module covers the event loop precisely, shows you how to measure saturation, and gives you the tools to offload work correctly.


The Six Phases of the Event Loop

The Node.js event loop is not a simple loop over a single queue. It is a phased loop — in each iteration, it processes up to six distinct phases, and the order is non-negotiable.

text

Between every phase and between every callback within a phase, Node.js drains two special queues:

  1. process.nextTick queue — highest priority micro-queue
  2. Promise microtask queue — second priority

These micro-queues drain completely before the next phase or callback runs. This ordering has critical implications for high-throughput ingestion.

Phase 1: Timers

Executes callbacks scheduled by setTimeout and setInterval whose thresholds have elapsed. The "threshold" is a minimum — the callback won't run before the specified time, but it may run later if other phases are busy.

Production implication: If your ingestion pipeline uses setTimeout(fn, 100) as a flush trigger for a batch write, that timer will not fire at exactly 100ms if the poll phase is busy processing incoming transaction data. At 50K events/sec, the poll phase can be continuously occupied, delaying timers by hundreds of milliseconds.

Phase 2: Pending Callbacks

Executes I/O callbacks that were deferred to the next loop iteration by the OS (typically some TCP errors). Rarely populated in normal operation.

Phase 3: Idle, Prepare

Internal to libuv. Not accessible from JavaScript.

Phase 4: Poll

The most important phase for I/O-intensive applications.

The poll phase does two things:

  1. Calculates how long to block waiting for new I/O events (0ms if there are pending timers or setImmediate callbacks, otherwise up to some calculated maximum)
  2. Processes I/O callbacks in the poll queue

For a blockchain indexer receiving a continuous stream of transactions: incoming socket data triggers epoll notifications → libuv adds callbacks to the poll queue → the poll phase drains the poll queue. As long as data keeps arriving, the poll phase stays busy.

The blocking calculation is critical: if the poll queue keeps filling faster than it drains, the event loop never moves past the poll phase. Timers don't fire. setImmediate callbacks don't run. This is event loop starvation.

Phase 5: Check

Executes setImmediate callbacks. This phase runs after the poll phase, not before. If you want code to run "soon" but after current I/O has been processed, setImmediate is correct. If you want code to run "immediately" (before any I/O callbacks), process.nextTick is correct.

javascript

Phase 6: Close Callbacks

Executes close event callbacks (socket.on('close', ...)). Cleanup only.


Microtask Queues: The Invisible Priority System

Before every phase transition and between every callback, Node.js drains microtask queues in priority order:

Priority 1: process.nextTick queue Priority 2: Promise resolution queue (queueMicrotask, .then, await)

Both queues drain completely before the event loop moves forward. This has a dangerous implication: if you continuously add items to these queues, the event loop never advances.

process.nextTick and Promise microtasks are like an airport's expedite lane that must be completely empty before the main line can move at all — if passengers keep cutting into it, the main line never advances no matter how short it looks from outside. A phase transition is the main line; the microtask queues are the expedite lane. Add faster than it drains, and everything behind it — I/O callbacks, timers, setImmediate — simply waits.

The Microtask Starvation Pattern

javascript
javascript

Promise Chain Depth and Starvation

javascript

Event Loop Utilization (ELU): The Critical Production Metric

Event Loop Utilization (ELU) measures the ratio of time the event loop spends actively executing JavaScript vs idling in the poll phase waiting for I/O.

text
javascript
javascript

Alert thresholds for a UPI payment gateway:

ELUStatusAction
< 0.70HealthyNo action
0.70–0.85WatchProfile for CPU hotspots
0.85–0.95WarningOffload CPU work, scale
> 0.95CriticalImmediate intervention, add instances

Event Loop Lag: Measuring Actual Delay

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.