발송 stuck 진단 — lead-on-demand-enrich 가 sequence-email 막음
2026-06-05 · 분당 1-3건 → 진짜 원인 = Hunter API 429 + event loop 점유
한 줄 결론
발신계정 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 active | 25 | 🔴 retry 폭주 |
lead-on-demand-enrich waiting | 86,187 | 대규모 적체 |
| 최근 2분 Hunter 429 에러 | 22회 | 계속 진행 중 |
sequence-email active | 0 | ⚠ idle |
sequence-email delayed | 48,789 | 모두 미래 scheduled (overdue 0) |
| 최근 2분 sequence-email completed | 0 | 발송 0 |
| 최근 1분 발송 (DB sent) | 7 | 분당 7건 |
| worker CPU | 8.63% | idle 보이지만 event loop 점유 |
| DB step1 pending overdue (글로벌 식품 + AI 6/2) | 30,339 | enqueue 안 됨 |
2. lead-on-demand-enrich 는 무슨 기능?
리드별 이메일 enrichment 워커 — 영업 자동화의 lead 발견·검증 파이프라인.
처리 단계 (per lead)
- Cache check — 30일 Redis cache hit 시 skip
- 예산 reservation — workspace 의 월 USD ceiling 에서 Lua atomic 예약
- 크레딧 차감 — atomic deduct, 0 이면
InsufficientCreditsError - lead_enrichment_state 상태
enriching으로 mark - 외부 API 호출 — Hunter / Findymail / MillionVerifier
- 예산 reservation settle (실 사용분만 청구)
- 결과 저장 —
lead_enrichment_state+lead_emailsledger - refund 정책 (unreachable / failed 시)
- SSE event publish (
enrichment:${workspaceId}) awaiting_enrichmentstep_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-enrich와sequence-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 의존성 문제.