Node.js 서버가 갑자기 느려졌는데 CPU 사용률이나 에러율만 봐서는 이유가 잘 안 보일 때가 있습니다. 이럴 때 꽤 자주 놓치는 신호가 Event Loop Lag입니다. 이 값이 커지면 요청 처리는 살아 있어 보여도 실제 응답은 늦어지고, timeout, queue 적체, tail latency 상승이 함께 따라옵니다.

특히 Node.js는 단일 이벤트 루프 위에서 많은 작업이 돌아가기 때문에, 짧은 동기 블로킹이나 CPU 집중 작업이 쌓이면 전체 서비스 품질이 빠르게 흔들릴 수 있습니다. 이 글에서는 Node.js Event Loop Lag가 무엇인지, 어떤 방식으로 모니터링해야 하는지, 그리고 운영에서 어떤 액션으로 이어져야 하는지를 실무 기준으로 정리합니다.

Node.js Event Loop Lag란 무엇인가

H3. 이벤트 루프가 다음 작업을 제때 실행하지 못한 지연이다

Event Loop Lag는 예약된 작업이 원래 실행돼야 할 시점보다 얼마나 늦게 실행됐는지를 보여주는 지표입니다. 쉽게 말하면, 이벤트 루프가 너무 바빠서 다음 tick을 바로 처리하지 못한 시간입니다.

이 값이 커진다는 것은 대체로 아래 중 하나를 의미합니다.

  • CPU 바운드 작업이 메인 스레드를 오래 점유함
  • 동기식 I/O 또는 무거운 JSON 처리로 루프가 막힘
  • 너무 많은 콜백이나 마이크로태스크가 한 번에 몰림
  • GC 구간이나 비정상적인 메모리 압박이 발생함

즉 Event Loop Lag는 단순 성능 숫자가 아니라, Node.js 런타임이 얼마나 숨 가쁘게 일하고 있는지 보여주는 건강 지표에 가깝습니다.

H3. 평균 응답시간보다 먼저 이상 징후를 보여줄 수 있다

서비스가 완전히 느려진 뒤에야 latency 그래프가 올라가는 경우도 많습니다. 반면 Event Loop Lag는 실제 장애가 커지기 전부터 이상 신호를 주는 편입니다.

예를 들어 아래처럼 해석할 수 있습니다.

  • CPU는 아직 70% 수준인데 p99 latency가 흔들리기 시작함
  • DB는 멀쩡한데 애플리케이션 레벨 timeout이 늘어남
  • 특정 배치 작업 시간대에만 응답 지연이 커짐
  • 헬스체크는 통과하지만 사용자 체감 속도는 나빠짐

이럴 때 Event Loop Lag를 함께 보면, 인프라 문제가 아니라 애플리케이션 메인 스레드 경쟁 문제라는 힌트를 더 빨리 얻을 수 있습니다.

왜 Node.js 운영에서 Event Loop Lag를 꼭 봐야 하나

H3. 단일 스레드 병목은 연쇄 지연으로 번지기 쉽다

Node.js는 비동기 I/O에 강하지만, 메인 이벤트 루프를 오래 붙잡는 작업에는 취약합니다. 한 요청의 동기 블로킹이 길어지면 다른 요청의 콜백도 함께 밀립니다. 이 때문에 문제는 한 엔드포인트에만 머물지 않고 전체 서비스 응답성 저하로 번질 수 있습니다.

특히 아래 현상과 함께 보이면 위험합니다.

  • p95보다 p99 latency가 더 가파르게 상승함
  • timeout과 deadline exceeded 에러가 같이 증가함
  • queue wait, connection acquire wait 같은 대기 시간이 늘어남
  • 요청량이 크게 늘지 않았는데도 서버가 갑자기 버벅거림

이런 상황은 과부하 자체보다 과부하가 루프 지연으로 전파되는 구조를 먼저 봐야 합니다. 관련해서 전체 지연 확산을 막는 관점은 Node.js Load Shedding 가이드와도 연결됩니다.

H3. CPU 사용률만으로는 메인 스레드 문제를 정확히 설명하기 어렵다

CPU가 높으면 당연히 의심할 수 있지만, CPU만 보고 있으면 놓치는 케이스가 있습니다. 예를 들어 짧고 잦은 동기 블로킹은 평균 CPU 그래프보다 Event Loop Lag에서 더 선명하게 드러납니다.

반대로 CPU가 높아도 worker thread나 외부 프로세스에서 소비되는 것이라면 메인 이벤트 루프 지연과는 결이 다를 수 있습니다. 그래서 운영에서는 아래를 같이 봐야 합니다.

  • Event Loop Lag
  • CPU 사용률
  • GC pause
  • request latency
  • error rate와 timeout 수

이 조합이 있어야 “서버가 바쁜가”가 아니라 “메인 루프가 막히는가”를 더 정확히 판단할 수 있습니다. CPU 바운드 작업을 분리하는 관점은 Node.js Worker Threads 가이드도 함께 참고할 만합니다.

Node.js Event Loop Lag는 어떻게 측정하나

H3. monitorEventLoopDelay로 p95, p99를 보는 방식이 실무적이다

Node.js에서는 perf_hooksmonitorEventLoopDelay를 사용해 Event Loop Lag 분포를 측정할 수 있습니다. 실무에서는 평균보다 p95, p99 같은 상위 구간을 보는 편이 훨씬 유용합니다.

const { monitorEventLoopDelay } = require('node:perf_hooks');

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

setInterval(() => {
  const p95Ms = histogram.percentile(95) / 1e6;
  const p99Ms = histogram.percentile(99) / 1e6;
  const maxMs = histogram.max / 1e6;

  console.log({ p95Ms, p99Ms, maxMs });
  histogram.reset();
}, 10000);

이렇게 10초 또는 30초 단위로 수집하면, 특정 시간대에만 튀는 지연도 비교적 잘 포착할 수 있습니다. 중요한 것은 숫자 하나보다 분포와 추세입니다.

H3. 응답시간 지표와 따로 두지 말고 함께 묶어 본다

Event Loop Lag만 단독으로 봐도 의미는 있지만, 운영 의사결정에는 한계가 있습니다. 아래 지표와 나란히 놓아야 액션으로 이어지기 쉽습니다.

  • endpoint별 p95, p99 latency
  • active handles, active requests
  • heap usage와 GC 관련 지표
  • timeout, 5xx, aborted request 수
  • queue length 또는 pool acquire wait

예를 들어 Event Loop Lag만 올랐는데 DB 지표는 안정적이라면, 원인은 SQL보다 애플리케이션 코드일 가능성이 큽니다. 반대로 Lag와 함께 timeout이 증가한다면 Node.js Deadline Exceeded 가이드처럼 요청 예산 초과가 어떤 계층에서 시작되는지도 함께 봐야 합니다.

어느 정도 수치부터 경계해야 하나

H3. 절대값보다 평소 기준선과 피크 시점 변화가 더 중요하다

Event Loop Lag는 서비스 성격에 따라 정상 범위가 다릅니다. 실시간성이 높은 API와 내부 배치 서버는 허용 기준이 같을 수 없습니다. 그래서 무조건적인 한 숫자보다, 평소 기준선 대비 얼마나 상승했는지를 먼저 봐야 합니다.

그래도 운영 출발점으로는 아래 정도가 실용적입니다.

  • p95 lag 20ms 이하: 대체로 안정적
  • p95 lag 20~50ms: 주의 구간, 지표 추세 확인
  • p95 lag 50ms 이상: 사용자 체감 지연 가능성 큼
  • p99 lag 100ms 이상 반복: 블로킹 작업 추적 필요

중요한 것은 임계치를 정한 뒤, 이를 알림과 운영 대응 규칙으로 연결하는 것입니다. 숫자만 대시보드에 있어서는 큰 도움이 되지 않습니다.

H3. 배포, 배치, 트래픽 피크와 같이 맥락을 붙여야 한다

같은 60ms lag라도 상황에 따라 의미가 다릅니다. 배포 직후 잠깐 튀는 것과, 평일 오전 내내 반복되는 것은 운영 우선순위가 다릅니다.

실무에서는 아래 태그를 함께 남기는 편이 좋습니다.

  • deploy version
  • job name 또는 cron 실행 여부
  • endpoint group
  • instance 또는 pod id
  • CPU throttling 여부

이런 맥락이 있어야 “왜 오늘만 튀었는지”를 더 빨리 좁힐 수 있습니다.

Event Loop Lag가 커질 때 먼저 의심할 원인

H3. 동기 코드와 무거운 직렬화 작업

생각보다 흔한 원인은 복잡한 알고리즘보다 평범한 동기 코드입니다. 예를 들면 아래 같은 패턴입니다.

  • 대용량 JSON.stringify 또는 JSON.parse
  • 요청마다 큰 배열을 동기 정렬하거나 순회함
  • fs.readFileSync, crypto.pbkdf2Sync 같은 동기 API 사용
  • 템플릿 렌더링이나 압축을 메인 스레드에서 과하게 수행함

이 문제는 로직이 맞더라도, 실행 위치가 잘못돼 서비스 전체 지연으로 이어질 수 있습니다.

H3. CPU 바운드 작업이 API 서버에 섞여 들어오는 경우

이미지 처리, 리포트 생성, 대규모 데이터 변환처럼 CPU를 오래 쓰는 작업이 API 요청 경로 안에 섞이면 Event Loop Lag가 쉽게 커집니다. 이럴 때는 단순 튜닝보다 작업 격리가 더 효과적입니다.

보통 아래 선택지가 실무적입니다.

  • Worker Threads로 분리
  • 별도 job worker 프로세스로 이동
  • 요청 경로에서는 비동기 큐에 위임
  • 피크 시간대에는 동시 실행 수 제한

즉 Lag가 높다는 사실만 보는 것이 아니라, 그 일을 꼭 메인 이벤트 루프가 해야 하는지부터 다시 판단해야 합니다.

운영에서 바로 적용할 수 있는 대응 패턴

H3. 느린 요청보다 느린 루프를 먼저 경고하도록 만든다

많은 팀이 latency 알림만 두고 Event Loop Lag 알림은 생략합니다. 하지만 실제로는 Lag 경고가 더 빠른 선행 지표가 되는 경우가 많습니다.

예를 들면 아래처럼 운영 규칙을 둘 수 있습니다.

  • 5분 평균 p95 lag 40ms 초과 시 경고
  • p99 lag 100ms 초과가 3회 연속이면 조사 시작
  • lag 상승과 timeout 증가가 함께 발생하면 우선순위 상향
  • 특정 배치 실행 시간과 겹치면 자동 태깅

이런 규칙은 과도하게 복잡할 필요는 없습니다. 핵심은 사용자 장애가 커지기 전에 먼저 잡는 것입니다.

H3. 메인 스레드에서 해야 할 일과 하지 말아야 할 일을 구분한다

가장 효과가 큰 개선은 코드 최적화보다 역할 분리인 경우가 많습니다. Event Loop Lag를 안정적으로 줄이려면 아래 기준이 도움이 됩니다.

  • 요청 응답에 꼭 필요한 작업만 메인 경로에 둔다
  • 큰 직렬화, 압축, 암호화는 별도 처리 가능성을 검토한다
  • 동기 API 사용을 점검하고 비동기 또는 외부 worker로 옮긴다
  • 동일 시점에 몰리는 작업에는 큐와 동시성 제한을 둔다

이 기준은 단순 성능 문제가 아니라 시스템 보호 전략입니다. 특히 과부하 구간에서는 Node.js Connection Pool Exhaustion 가이드처럼 다른 병목과 함께 번지기 쉽기 때문에, 루프 지연을 조기에 줄이는 편이 훨씬 안전합니다.

실무 점검 체크리스트

H3. 운영 전에 이 다섯 가지는 확인한다

Node.js에서 Event Loop Lag를 모니터링 체계에 넣을 때는 아래 항목부터 맞추면 좋습니다.

  • monitorEventLoopDelay 또는 동등한 방식으로 p95, p99를 수집하는가
  • latency, timeout, CPU, GC 지표와 같은 대시보드에서 볼 수 있는가
  • 배포, 배치, 특정 엔드포인트와 연관관계를 추적할 수 있는가
  • 동기 API와 CPU 바운드 작업 위치를 정기적으로 점검하는가
  • 임계치 초과 시 대응 절차가 정해져 있는가

이 다섯 가지가 정리되면 Event Loop Lag는 단순 참고 지표가 아니라, 실제 장애 예방 장치로 작동합니다.

마무리

Node.js Event Loop Lag는 “서버가 좀 느린가 보다” 정도로 넘기기 쉬운 숫자지만, 실제로는 서비스 품질 저하를 가장 빨리 알려 주는 신호 중 하나입니다. 응답시간이 흔들리고 timeout이 늘기 시작했다면, CPU와 DB만 보지 말고 이벤트 루프가 얼마나 늦어지고 있는지도 함께 확인하는 편이 좋습니다.

핵심은 간단합니다. 느린 요청을 보는 것만으로는 부족하고, 느린 이벤트 루프를 함께 봐야 합니다. 그 기준이 있어야 메인 스레드를 막는 코드와 작업 패턴을 빨리 찾고, Node.js 서비스의 지연 급증을 더 작은 범위에서 멈출 수 있습니다.