Event loop delay is how late a timer fires: schedule something 10 ms out, measure when it runs, and the excess is time the loop spent where it could not be interrupted. It predicts tail latency, because every pending request waits behind the same block. monitorEventLoopDelay() samples it inside libuv into an HDR histogram in nanoseconds — stable since Node 11.10.0 and cheap enough to leave on in production.
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
let ticks = 0;
const timer = setInterval(() => {
const end = performance.now() + 220;
if (++ticks === 20) while (performance.now() < end); // one slow request
if (ticks < 40) return;
clearInterval(timer);
h.disable();
const ms = (ns) => (ns / 1e6).toFixed(1);
const elu = performance.eventLoopUtilization().utilization.toFixed(3);
console.log(`mean ${ms(h.mean)} p50 ${ms(h.percentile(50))} p99 ${ms(h.percentile(99))}` +
` max ${ms(h.max)} ms, utilization ${elu}`);
}, 10);mean 22.0 p50 16.1 p99 240.3 max 240.3 ms, utilization 0.256
The median is unremarkable and the p99 is a disaster: one handler in forty held the loop for 220 ms and every callback behind it inherited the delay. An average hides exactly the event you are hunting. eventLoopUtilization() answers the other half: 0.256 means the loop was busy 26% of the time, so this process is not saturated, it is intermittently blocked. At 0.95 it needs more instances; at 0.26 with a bad p99 it needs the blocking work moved to a worker thread (Worker Threads).