28 июл. 2026 г.

Event loop lag в Node.js: почему API отвечает медленно, хотя база не виновата

Event loop lag помогает увидеть проблемы, которые не объясняются медленной базой: CPU-bound код, тяжелая сериализация, sync API и длинные microtask-цепочки.

Когда backend начинает отвечать медленно, первая мысль обычно такая: база тормозит, сеть лагает, Redis не успевает, внешний сервис долго отвечает. Иногда это правда. Но в Node.js есть еще один важный источник задержек - event loop lag.

Event loop lag появляется, когда основной поток Node.js занят и не может вовремя перейти к следующей задаче. Запросы уже пришли, timers уже готовы, socket events уже ждут, но JavaScript все еще выполняет синхронный код.

Почему это неприятно

Node.js часто используют для I/O-heavy backend: REST API, GraphQL, WebSocket, очереди, интеграции, работа с базой. В таких задачах модель работает хорошо: пока база или сеть делают свою работу, event loop может обслуживать другие события.

Но если в request handler попадает тяжелая синхронная операция, весь процесс становится менее отзывчивым. Один пользователь может запустить обработку, из-за которой остальные запросы начинают ждать.

Типичные симптомы:

  • p95/p99 latency растет без очевидной причины;
  • база отвечает быстро, но API все равно медленный;
  • WebSocket-события приходят с задержкой;
  • timers и scheduled jobs выполняются позже ожидаемого;
  • healthcheck иногда отвечает медленно, хотя сервис “живой”;
  • CPU процесса высокий, но I/O не выглядит перегруженным.

Частые причины

Самые частые источники event loop lag в backend:

  • большой JSON.stringify или JSON.parse;
  • обработка больших массивов в синхронном цикле;
  • генерация Excel/PDF/документов в основном потоке;
  • синхронные filesystem операции;
  • тяжелая криптография без worker'ов;
  • image processing;
  • неудачные регулярные выражения;
  • слишком длинные Promise/microtask цепочки;
  • логирование огромных объектов.

В коде это может выглядеть невинно:

const rows = await this.repository.findMany(filters);
const enriched = rows.map((row) => heavyTransform(row));
return { items: enriched };

Проблема не в await. После того как данные пришли из базы, map выполняется синхронно. Если rows большой, event loop занят до конца обработки.

Как я думаю о таких местах

Я стараюсь разделять I/O и CPU work. I/O можно параллелить через асинхронную модель Node.js. CPU work нужно ограничивать, выносить или дробить.

Практические вопросы:

  • сколько элементов может прийти в этот endpoint;
  • есть ли жесткий limit на пагинацию;
  • можно ли делать transform на уровне SQL;
  • можно ли стримить результат;
  • нужно ли выносить генерацию в очередь;
  • можно ли перенести тяжелую часть в worker thread;
  • что будет, если 20 пользователей одновременно запустят этот сценарий.

Если ответ “процесс просто посчитает”, это часто плохой ответ для Node.js backend.

Очередь не всегда решает проблему

Вынести задачу в BullMQ или другую очередь полезно, но важно понимать: если worker работает в том же Node.js процессе и выполняет тяжелый синхронный код, он тоже может блокировать свой event loop.

Очередь помогает:

  • убрать долгую работу из пользовательского HTTP-запроса;
  • повторять задачи;
  • контролировать concurrency;
  • хранить статус процесса;
  • логировать ошибки.

Но для CPU-heavy задач иногда нужен отдельный worker process, worker threads или отдельный сервис. Очередь - это способ организовать выполнение, а не автоматическое ускорение вычислений.

Как мониторить

В Node.js можно измерять event loop delay через perf_hooks:

import { monitorEventLoopDelay } from 'node:perf_hooks';

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

setInterval(() => {
  const p95 = histogram.percentile(95) / 1_000_000;
  console.log({ eventLoopDelayP95Ms: p95 });
  histogram.reset();
}, 10_000);

Это не заменяет полноценный мониторинг, но помогает увидеть проблему: если задержка event loop растет вместе с latency, нужно искать blocking code, а не только медленные SQL-запросы.

Что я обычно делаю

Мой базовый checklist:

  • ставлю лимиты на списки и batch-операции;
  • избегаю sync filesystem API в request path;
  • не логирую большие payload целиком;
  • выношу генерацию документов и отчетов в background jobs;
  • контролирую concurrency у worker'ов;
  • для CPU-heavy задач рассматриваю worker threads или отдельный сервис;
  • проверяю регулярные выражения и большие JSON-операции;
  • добавляю метрики latency и event loop delay.

Вывод

Event loop lag - это напоминание, что Node.js асинхронный, но не магический. Он отлично переживает много I/O, но плохо относится к тяжелой синхронной работе в основном потоке.

Для backend-разработчика важно не просто знать фазы event loop, а уметь увидеть их последствия в production: растущую задержку, поздние timers, подвисающие WebSocket-события и request handlers, которые мешают друг другу.

Комментарии

Пока нет комментариев — будьте первым.