Event Loop Delay Detection & Internals

Deep dive into how overload-protection measures event loop lag, memory pressure, and thread contention.


How Event Loop Delay Detection Works

In Node.js, asynchronous I/O and scheduled timers are orchestrated by libuv on a single execution thread. When JavaScript executes synchronous (blocking) work, libuv cannot advance to subsequent phases:

sequenceDiagram
    autonumber
    participant App as Application Code
    participant UV as libuv Event Loop
    participant Hist as Native Histogram (perf_hooks)
    
    Note over UV,Hist: Normal Operation (No Lag)
    UV->>Hist: Background timer fires on time (~5ms resolution)
    Note over Hist: Measured Lag: ~0.00ms (Healthy)
    
    Note over App,UV: Synchronous CPU Work (16ms Crypto / JWT / Zod)
    App->>UV: Execute synchronous task
    Note over UV: Event loop frozen on thread for 16ms
    App->>UV: Task completes and yields thread
    UV->>Hist: Delayed sample fires (16ms elapsed)
    Note over Hist: Measured Lag: 16ms recorded in histogram

Native node:perf_hooks Engine

In modern Node.js, perf_hooks.monitorEventLoopDelay provides native C++ event loop sampling:

import { monitorEventLoopDelay } from 'node:perf_hooks'

// Replaces manual setInterval polling loops
const eventLoopMonitor = monitorEventLoopDelay({ resolution: 5 })
eventLoopMonitor.enable()

Why monitorEventLoopDelay is Superior

  1. Zero Polling Overhead: Unlike legacy JavaScript timers that wake the event loop and contend with application code, monitorEventLoopDelay is implemented in C++ using high-resolution monotonic clocks.
  2. Nanosecond Precision: Delays are recorded in high-resolution nanosecond buckets. Converting to milliseconds is as simple as dividing by 10⁶ (/ 1e6).
  3. Statistical Robustness (p99): By tracking percentiles (eventLoopMonitor.percentile(99)), transient single-tick anomalies are smoothed out while genuine sustained overload is caught instantly.

Why p99 (percentile(99) / 1e6) is the Gold Standard


Memory Metrics: Heap vs RSS

In addition to event loop delay, overload-protection can monitor memory consumption:

+--------------------------------------------------------------+
| Process Resident Set Size (RSS)                              |
| +----------------------------------------------------------+ |
| | V8 Total Heap Size                                       | |
| | +-----------------------+ +----------------------------+ | |
| | | V8 Used Heap Size     | | V8 Unallocated Heap Space  | | |
| | | (Active JS Objects)   | |                            | | |
| | +-----------------------+ +----------------------------+ | |
| +----------------------------------------------------------+ |
| +----------------------------------------------------------+ |
| | Native Buffers, C++ Bindings, libuv Thread Pool Memory   | |
| +----------------------------------------------------------+ |
+--------------------------------------------------------------+

1. maxHeapUsedBytes (V8 Used Heap)

2. maxRssBytes (Resident Set Size)


Profiler State & Prototype Inheritance

When using the multi-framework adapter:

const protect = require('overload-protection')('express', opts)

The returned middleware inherits from the underlying profiler prototype:

Property Type Description
instance.overload boolean true if any configured threshold is currently breached
instance.eventLoopOverload boolean true if event loop lag exceeds maxEventLoopDelayMs
instance.heapUsedOverload boolean true if heap used exceeds maxHeapUsedBytes
instance.rssOverload boolean true if RSS exceeds maxRssBytes
instance.eventLoopDelay number Live detected lag in milliseconds
instance.eventLoopMonitor object Native IntervalHistogram instance
instance.stop() function Disables monitoring and clears all internal timers