P0 · Incident

발송 stuck 진단 — lead-on-demand-enrichsequence-email 막음

2026-06-05 · 분당 1-3건 → 진짜 원인 = Hunter API 429 + event loop 점유

📅 2026-06-05 05:55 KST📦 beta production🚨 stuck 13분+

한 줄 결론

발신계정 18개 분산은 잘 됐지만, lead-on-demand-enrich worker 가 Hunter API 결제 한도 (429 too_many_requests) 에 부딪혀 retry 폭주 → 같은 Node.js process 의 sequence-email-loader 가 event loop 차례 못 얻어 overdue 30k+ 잡을 BullMQ 에 enqueue 못 함.

STAGGER 단축은 도움 안 됨 (overdue 잡이 enqueue 자체가 안 되므로). 진짜 fix = lead-enrich worker 일시 중지 또는 분리.

1. 현재 상태 (실측)

지표평가
lead-on-demand-enrich active25🔴 retry 폭주
lead-on-demand-enrich waiting86,187대규모 적체
최근 2분 Hunter 429 에러22회계속 진행 중
sequence-email active0⚠ idle
sequence-email delayed48,789모두 미래 scheduled (overdue 0)
최근 2분 sequence-email completed0발송 0
최근 1분 발송 (DB sent)7분당 7건
worker CPU8.63%idle 보이지만 event loop 점유
DB step1 pending overdue (글로벌 식품 + AI 6/2)30,339enqueue 안 됨

2. lead-on-demand-enrich 는 무슨 기능?

리드별 이메일 enrichment 워커 — 영업 자동화의 lead 발견·검증 파이프라인.

처리 단계 (per lead)

  1. Cache check — 30일 Redis cache hit 시 skip
  2. 예산 reservation — workspace 의 월 USD ceiling 에서 Lua atomic 예약
  3. 크레딧 차감 — atomic deduct, 0 이면 InsufficientCreditsError
  4. lead_enrichment_state 상태 enriching 으로 mark
  5. 외부 API 호출 — Hunter / Findymail / MillionVerifier
  6. 예산 reservation settle (실 사용분만 청구)
  7. 결과 저장 — lead_enrichment_state + lead_emails ledger
  8. refund 정책 (unreachable / failed 시)
  9. SSE event publish (enrichment:${workspaceId})
  10. awaiting_enrichment step_execution 재enqueue — sequence-email-worker 가 pickup 하도록
핵심 의존성: 5번 외부 API (Hunter) 가 결제 한도 초과 → 429 → retry 다수 → 같은 Node.js process 의 다른 큐 모두 영향.

3. 인과 사슬 (왜 발송 멈췄나)

1. Hunter API 결제 한도 초과 → 429 too_many_requests 응답 2. lead-on-demand-enrich worker (25 active, 86k waiting) → 매 잡마다 Hunter API 호출 → 429 → IDEMPOTENT_RETRY (attempts:3) → retry attempt 1 실패 → attempt 2 실패 → backoff 후 다시 3. Node.js event loop 점유 → HTTP request + JSON parse + retry timer → 같은 process 의 다른 worker 도 event loop turn wait 4. sequence-email-loader (30s tick) → DB scan (sequence_step_executions WHERE status='pending') → addSequenceEmailJobs 호출 시 lua script (Redis) wait → enqueue 처리율 ↓ 5. overdue 30,339건이 BullMQ 에 enqueue 안 됨 → 글로벌 식품 브랜드사 22,377 + AI 캠페인 6/2 7,962 → DB scheduled_at < NOW 인데 큐에 안 들어감 6. sequence-email worker (concurrency 40) → BullMQ delayed → active 전환할 잡 없음 → active=0, CPU 8% idle 상태 7. 결과: 13분 전 마지막 발송 후 거의 정지 (분당 1-3건)

4. STAGGER 단축이 도움 안 되는 이유

이전 가설: STAGGER_INTERVAL_MS = 10_000 누적으로 잡이 미래로 밀림.

실측 검증:

  • BullMQ delayed 48,804건 중 overdue (score < NOW) = 0
  • 즉 enqueue 된 잡은 모두 정상 stagger (계정별 idx 독립)
  • 발송돼야 할 30k+ 가 BullMQ 에 enqueue 자체가 안 됨
STAGGER 줄여도 enqueue 가 일어나지 않으면 효과 0. 진짜 병목은 enqueue path 가 event loop 차례를 못 얻는 것.

5. 권장 fix

P0 즉시 Hunter 429 흐름 차단

옵션효과구현
A. Hunter API 결제 한도 증액429 사라짐, lead-enrich 정상화Hunter 대시보드
B. lead-on-demand-enrich 큐 일시 PAUSE즉시 event loop 회복 → sequence-email 정상 발송queue.pause() 또는 BullMQ admin
C. Hunter 429 에러 시 즉시 fail (retry 제거)retry 폭주 차단worker handler 의 retry 정책 변경

P1 중기 Worker 분리

  • lead-on-demand-enrichsequence-email 을 별도 컨테이너로 분리
  • docker-compose 에 worker-sequence-email 신규 서비스
  • BullMQ Worker 는 큐별 독립이지만 Node.js process 는 동일 — process 분리 필요
  • 이전 분석 PR plan 참고

P2 장기 외부 API 다양화

  • Hunter 단일 의존성 제거 — NeverBounce / ZeroBounce / Findymail fallback chain
  • 429 시 fallback provider 자동 전환
  • per-workspace API key 분산

6. 분산 자체는 정상 — 18계정에 enrollment 분배 완료

이전 작업 성과는 보존되어 있음:

  • AI 캠페인 6/2: pool 19, 18계정 균등 (847-953/계정), max/min 1.13
  • 글로벌 식품 브랜드사: pool 19, 18계정 균등 (1,265-1,424/계정), max/min 1.126
  • 총 40,799 enrollment 재할당 완료, 5,617 + 16,554 보존 (이미 step1 sent)

발송 stuck 은 분산과 무관 — lead-enrich worker 의 외부 API 의존성 문제.