Event Loop Utilization, ou ELU, é uma métrica que estima quanto tempo o event loop do Node.js permaneceu ativo em relação ao tempo total observado. Ela ajuda a diferenciar um processo ocupado executando JavaScript ou callbacks de um processo que passou a maior parte do intervalo esperando por eventos.
A métrica faz parte de node:perf_hooks e é especialmente útil para diagnosticar saturação da thread principal. CPU do sistema, latência e event loop delay continuam importantes, mas ELU acrescenta uma visão do próprio runtime.
O que o ELU mede
O resultado possui três valores principais:
- idle: tempo em que o event loop permaneceu ocioso;
- active: tempo em que esteve ativo;
- utilization: proporção entre atividade e o intervalo total.
Uma utilização próxima de 1 indica que o event loop passou quase todo o intervalo ativo. Isso pode ser esperado durante um benchmark intenso, mas é um sinal de alerta quando a latência aumenta e o throughput deixa de crescer.
Primeira medição
import { performance } from 'node:perf_hooks';
let previous = performance.eventLoopUtilization();
setInterval(() => {
const current = performance.eventLoopUtilization(previous);
previous = performance.eventLoopUtilization();
console.log({
active: current.active,
idle: current.idle,
utilization: current.utilization,
});
}, 10_000).unref();A chamada com um valor anterior calcula a diferença entre dois momentos. Trabalhar com intervalos é mais útil que observar apenas o acumulado desde o início do processo.
Utilização não é porcentagem de CPU
ELU e CPU respondem a perguntas diferentes. Um callback pode permanecer ativo aguardando uma chamada nativa síncrona, enquanto a distribuição de CPU aparece em outra camada. Também é possível ter CPU alta em Worker Threads e ELU moderado na thread principal.
Monitore em conjunto:
- CPU do processo e do container;
- event loop utilization;
- event loop delay;
- p95 e p99;
- requisições por segundo;
- taxa de erros;
- fila de trabalho;
- garbage collection.
ELU e event loop delay
Event Loop Utilization mostra a proporção de atividade. Event loop delay mede quanto um timer ou callback foi atrasado. Um processo pode ter ELU alto sem atraso extremo se o trabalho estiver dividido em callbacks curtos. Também pode apresentar atraso importante durante pequenos períodos de bloqueio que se perdem em uma média longa.
Use monitorEventLoopDelay:
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();
let previous = performance.eventLoopUtilization();
setInterval(() => {
const elu = performance.eventLoopUtilization(previous);
previous = performance.eventLoopUtilization();
console.log({
elu: elu.utilization,
delayP99Ms: delay.percentile(99) / 1e6,
delayMaxMs: delay.max / 1e6,
});
delay.reset();
}, 10_000).unref();Os valores do histograma são fornecidos em nanossegundos, por isso o exemplo divide por um milhão.
Exportando para Prometheus
Uma aplicação pode transformar ELU em gauge:
import client from 'prom-client';
import { performance } from 'node:perf_hooks';
const eluGauge = new client.Gauge({
name: 'nodejs_event_loop_utilization_ratio',
help: 'Utilização do event loop no último intervalo',
});
let previous = performance.eventLoopUtilization();
setInterval(() => {
const delta = performance.eventLoopUtilization(previous);
previous = performance.eventLoopUtilization();
eluGauge.set(delta.utilization);
}, 5_000).unref();Evite labels de alta cardinalidade. Normalmente basta identificar serviço, ambiente, versão e instância por meio da configuração do coletor.
Interpretando valores altos
Não existe um único limite universal. Uma API com callbacks curtos pode trabalhar bem com ELU alto durante picos. O problema surge quando a utilização elevada coincide com:
- latência crescente;
- queda de throughput;
- timeouts;
- event loop delay alto;
- fila aumentando;
- CPU próxima ao limite;
- autoscaling tardio;
- erros de dependências.
Crie uma baseline por serviço e compare o comportamento sob carga normal, pico e stress.
Exemplo de bloqueio
import { createServer } from 'node:http';
function trabalhoPesado() {
let total = 0;
for (let i = 0; i < 200_000_000; i += 1) {
total += Math.sqrt(i);
}
return total;
}
createServer((req, res) => {
if (req.url === '/calcular') {
res.end(String(trabalhoPesado()));
return;
}
res.end('ok');
}).listen(3000);Durante chamadas concorrentes, ELU tende a subir, o delay aumenta e outras rotas ficam lentas. A solução pode envolver reduzir o trabalho, armazenar resultado, paginar, processar em lote ou mover CPU-bound para Worker Threads.
Operações assíncronas também podem saturar
Uma aplicação pode usar APIs assíncronas e ainda manter o event loop ocupado processando milhares de callbacks, promises e serializações. “Assíncrono” não significa “sem custo de CPU”.
const resultados = await Promise.all(
ids.map(async (id) => {
const item = await buscarItem(id);
return transformarObjetoGrande(item);
}),
);Se a lista é enorme, resolução de promises e transformação podem dominar a thread. Use concorrência limitada, lotes, streams ou filas.
Medindo por requisição
ELU é uma métrica do processo ou da thread, não de uma única requisição. Tentar atribuir o valor diretamente a um usuário pode causar interpretações erradas. Para requisições individuais, use timers, traces e spans.
Você pode registrar o ELU no início e no fim de uma operação para fins experimentais, mas requisições concorrentes compartilham o mesmo event loop. O delta inclui trabalho de todas elas.
Worker Threads
Cada Worker Thread possui seu próprio event loop e sua própria medição. Monitore o worker dentro do próprio contexto e agregue os resultados com cuidado.
import { parentPort } from 'node:worker_threads';
import { performance } from 'node:perf_hooks';
let previous = performance.eventLoopUtilization();
setInterval(() => {
const delta = performance.eventLoopUtilization(previous);
previous = performance.eventLoopUtilization();
parentPort?.postMessage({ type: 'elu', value: delta.utilization });
}, 5_000).unref();Uma média simples pode esconder um único worker saturado. Preserve percentis ou valores por worker em diagnóstico interno, evitando cardinalidade excessiva no backend de métricas.
Cluster e múltiplos processos
Em cluster, cada processo possui ELU independente. O balanceador pode distribuir carga de forma desigual por conexões persistentes, afinidade ou requisições muito diferentes. Analise cada PID e também uma visão agregada.
Autoscaling
CPU é uma métrica comum para HPA, mas ELU pode indicar pressão na thread principal antes de a média de CPU do pod ficar clara. Uma estratégia possível é exportar ELU, criar uma métrica externa e combinar com fila, latência e CPU.
Não escale apenas por ELU sem testar. Um bloqueio causado por código síncrono pode ser multiplicado em mais réplicas sem resolver a causa.
Alertas
Evite alertar por um único valor instantâneo. Prefira uma condição sustentada e correlacionada:
ELU alto por 10 minutos
E p99 acima do SLO
E throughput sem crescimentoUma regra composta reduz ruído durante tarefas curtas, aquecimento ou manutenção.
Intervalo de coleta
Intervalos curtos mostram picos, mas geram mais ruído. Intervalos longos suavizam bloqueios breves. Cinco a quinze segundos costuma ser um ponto de partida para métricas operacionais; investigações podem usar janelas menores.
Use o mesmo intervalo ao comparar versões. Reiniciar o valor anterior incorretamente pode produzir deltas inconsistentes.
ELU no início do processo
Bootstrap, carregamento de módulos, migrations e aquecimento podem elevar ELU. Marque a aplicação como pronta somente após concluir inicialização crítica. Separe dashboards de startup e tráfego estável.
Testando uma otimização
- estabeleça carga e baseline;
- registre ELU, delay, CPU, throughput e percentis;
- capture perfil de CPU;
- faça uma alteração pequena;
- repita na mesma infraestrutura;
- compare medianas de várias execuções;
- valide correção funcional.
Uma redução de ELU não é automaticamente positiva se o trabalho foi deslocado para uma dependência e a latência piorou.
Erros comuns
- tratar ELU como CPU;
- usar um limite universal sem baseline;
- observar apenas média longa;
- ignorar event loop delay;
- agregar workers de forma que esconda saturação;
- criar labels por PID ou requisição no Prometheus;
- escalar sem corrigir código bloqueante;
- medir somente em ambiente ocioso.
Fluxo recomendado
Meça ELU em intervalos, correlacione com delay, CPU e latência, estabeleça uma baseline e use profiling quando a utilização indicar saturação. Combine com CPU Profiling com –prof, Clinic.js, Autocannon e métricas em Prometheus no Node.js.
Consulte a documentação oficial de eventLoopUtilization e a referência oficial de monitorEventLoopDelay.



