Sequence Email Worker · Step 2 (verify-email)

발송 직전 이메일 검증 단계의 변천사

2026-04 ~ 2026-05 사이 sequence-email-worker 의 발송 직전 검증 단계는 MV 단발 → 3-tier cascade → cheap 게이트만 세 형태로 두 번 크게 바뀌었다. 각 시기의 개념·호출 흐름·캐시 정책·실패 처리·도입 배경·제거 배경을 한 곳에 정리.

📁 elysia-server/src/workers/bullmq/sequence-email-worker/steps/verify-email.ts

TL;DR

2025-12-30 ~ 2026-03-09 · 워커-내 검증 호출의 진짜 최초Mohamed Magdy (PR #532)email-sequence-worker-v2.tsverifyEmail() 호출을 처음 넣음. Hunter.io 단일 verify + Hunter.io domain search fallback.

2026-03-09 ~ 2026-05-06 · Cheolhee Lee (PR #1978) 가 Hunter.io → MillionVerifier multi-signal pipeline 으로 전면 교체. Redis ev:cache + in-process MX 24h + DB history 30d + KR 도메인 보정 + Option B (API 실패 시 MX fallback). 실패 시 step skip (lead-level dead-end 아님).

2026-05-06 ~ 2026-05-21 · Ahmed Mansy (PR #6594) 가 MV → Findymail → Hunter 3-tier cascade 도입 + fail-CLOSED 으로 enrollment 비가역 stop. 이게 사고의 직접 원인.

2026-05-21 이후 (현재) · 이예인 (PR #7787) 가 유료 cascade 전체 제거. 무료 게이트(format/role/dummy/disposable/blacklist/dedup)만 잔존. 책임은 upstream(buyer-search·enrichment·CSV import)으로 이동.

제안 — Hybrid 디자인 · ① 시기 그대로 복원은 비추천. SSOT(lead_contacts.is_verified) 신뢰 + Phase 0~5 보강 + ① 정신(MV multi-signal · fail-OPEN) 을 합친 4-Tier 게이트. 자세히 ↓

전체 타임라인

phase 0 · Hunter.io 단일 2025-12-30 ~ 2026-03-09 Hunter.io verify + domain search PR #529/#532 (Mohamed Magdy). 워커-내 verifyEmail() 호출의 최초. undeliverable 이면 같은 도메인 다른 주소 fallback.
phase 1 · MV 단발 2026-03-09 ~ 2026-05-06 MillionVerifier multi-signal PR #1978 (Cheolhee Lee). Hunter → MV 교체 + DB history/MX 시그널 + KR 도메인 보정 + Option B fallback + null-as-format-error 픽스.
phase 2 · 3-tier cascade 2026-05-06 ~ 2026-05-21 MV → Findymail → Hunter PR #6594 (Ahmed Mansy). fail-CLOSED → enrollment stopped(unreachable). 5/8 #6947 로 Phase 0~5 신뢰성 강화.
phase 3 · 현재 2026-05-21 ~ cheap 게이트만 PR #7787 (이예인). cascade 호출 자체 제거. step-level skip 만. upstream 으로 검증 책임 이관.

⓪ 2025-12-30 ~ 2026-03-09 · Hunter.io 단일 시기

이 단계가 sequence 발송 워커에 검증 호출이 처음 들어간 시점. 도입자는 Mohamed Magdy. elysia-server/src/workers/email-sequence-worker-v2.ts (당시 워커 파일명) 에 verifyEmail(leadContact.email) 호출 + Hunter.io domain search fallback 을 같이 넣었다.

개념

Hunter.io 의 단일 verify API 호출. multi-signal pipeline 없음 — DB 발송 이력도 MX 캐시도 KR 도메인 보정도 없다. 이 시기 가장 흥미로운 점은 undeliverable 판정 시 같은 도메인의 다른 주소를 찾아 자동 교체하는 fallback 분기가 있었다는 것.

호출 흐름

processSequenceEmailJob
  1. leadContact.email  획득
  2. verifyEmail(leadContact.email)               // hunterio-email-verifier.service
  3. result === "undeliverable"
       ├─ Gemini enrichment 에서 받은 대체 주소 → verifyEmail 재실행 → ok 면 채택
       └─ 그래도 없으면: searchDomainAllEmails(domain, 5) (Hunter.io domain search)
           └─ 각 후보를 verifyEmail() 로 재검증 → ok 첫 후보 채택
  4. 모두 실패 → step skip
  5. ok → 발송 진행

특징

알려진 약점 (→ PR #1978 이 해결)

관련 커밋

① 2026-03-09 ~ 2026-05-06 · MV 단발 시기

도입자 Cheolhee Lee, PR #1978 (커밋 06e3c47ce). 커밋 메시지 발췌: "Replace simple Hunter.io-based verification with a robust multi-signal pipeline — MillionVerifier API + DB delivery/bounce history + MX record + Korean domain FP correction + Option B (API failures return MX-based fallback instead of null) + Fix null-passes-as-valid bugs in worker."

개념

Step 2 의 verification 은 MillionVerifier 한 곳만 호출했다. 단, MV 호출 자체가 multi-signal pipeline 으로 래핑되어 있어 호출 전에 Redis 캐시 → DB 발송/바운스 이력 → MX 레코드 순서로 단서를 모으고, 시그널이 충분하면 MV API 를 스킵한다. API 장애 시에는 null 을 던지지 않고 MX-기반 fallback result 를 만들어 downstream 분기를 통일.

호출 흐름 (verifyAndDedup)

verifyAndDedup(toEmail)
  └─► verifyEmail(toEmail)                          // million-verifier.service
        1. isValidEmailFormat          → null 반환 = format error
        2. Redis cache  ev:<email>     → 히트 시 즉시 반환
        3. parallel:
             ├─ checkDeliveryHistory(email)   // emails 테이블 30d 윈도우
             └─ checkMxRecords(domain)        // DNS, in-process Map 24h
        4. history.bounced  → undeliverable result   (API skip)
           history.delivered → deliverable score=99  (API skip, MV 95 보다 높음)
        5. !hasMx           → undeliverable result   (API skip)
        6. callMillionVerifierApi(email)    // PQueue 144/sec
             ├─ ok                  → transformMvResponse
             ├─ credits ≤ 0         → CreditsExhaustedError throw
             ├─ 401/403/429/5xx     → buildApiFallbackResult(MX 기반)
             └─ schema fail / null  → buildApiFallbackResult
        7. applyKoreanDomainCorrection (.kr/.한국 → invalid 면 risky 완화)
        8. cache.set(result)
        9. return EmailVerificationResult { status, result, score, … }

  └─► verifyAndDedup 의 분기:
        result === null                       → failed   (format)
        result.result === "undeliverable"     → failed
        result.result === "risky" && score≤20 → skipped
        DUMMY_PATTERNS 매칭                    → skipped
        !isValidEmailFormat                   → failed
        existingSend dedup                    → skipped
        → ok

캐시 정책

레이어저장소TTL역할
L1 · 검증 결과 캐시 Redis ev:<email> config.cache.emailVerification verifyEmail 의 최종 result 캐싱. 같은 주소 재발송 시 MV API 호출 0회.
L2 · MX 캐시 워커 in-process Map 도메인 (lowercase) 24h DNS resolveMx 결과. 워커 재시작마다 콜드.
L3 · DB 발송 이력 시그널 Postgres emails to_email 30d 윈도우 bounced 1건이라도 있으면 즉시 undeliverable. delivered 있으면 score 99 로 API skip.
요청 throttle in-process PQueue 144 req/s (MV 한도 160/s 대비 10% safety margin).

실패 처리 (중요)

step-level skip 만 한다 — lead-level dead-end 아님. 한 step 이 undeliverable/risky/dummy 로 빠지면 step_executions.status='skipped'/'failed' 만 찍고 다음 step 으로 넘어간다. sequence_enrollments 의 상태에는 손대지 않음.

알려진 약점 (#6947 회고 기준)

대표 시그니처

// million-verifier.service.ts
export async function verifyEmail(email: string): Promise<EmailVerificationResult | null>

// verify-email.ts (worker step)
export async function verifyAndDedup(
  ctx: JobContext,
  toEmail: string,
): Promise<{ ok: true } | { ok: false; result: SequenceEmailResult }>

② 2026-05-06 ~ 2026-05-21 · 3-tier cascade 시기

개념

PR #6594 (Ahmed Mansy, lead-discovery 리워크에 묶여서 들어옴) 가 MV → Findymail → Hunter 3단 cascade 를 도입. 각 tier 가 send / catch_all / inconclusive 판정을 내리고, 마지막 tier 까지 합의 실패면 verdict = "block". 가장 큰 변화는 verdict 가 block 일 때 단순 step skip 이 아니라 sequence_enrollments.status = 'stopped', stop_reason = 'unreachable' 으로 enrollment 자체를 비가역 종료한다는 점.

호출 흐름

verifyAndDedup(toEmail)
  1. isValidEmailFormat (format pre-check, cascade 호출 전)
  2. cascade = verifyEmailCascade(toEmail, { caller: "send_time" })
       ├─ Tier 1: MillionVerifier        (consensus cache 우선)
       ├─ Tier 2: Findymail              (catch_all 의심 시 escalate)
       └─ Tier 3: Hunter                 (마지막 합의 시도)
       → verdict = "send" | "skip" | "block"
  3. verdict === "block"
       ├─ step_executions.status = 'skipped'
       ├─ sequence_enrollments.status = 'stopped'
       │    stop_reason = 'unreachable'
       │    stop_note   = 'verification_cascade:<blockReason>'
       └─ 동일 enrollment 다시 못 깨움  ← fail-CLOSED 비가역
  4. dummy / dedup gate (기존 그대로)

Phase 0~5 신뢰성 강화 (#6947, 2026-05-08)

도입 2일 만에 Hojin Kang 이 cascade 위에 무료 보강 5단계를 올림. 유료 API 추가 없이 약점 제거 목적.

Phase내용위치Default
0Shadow telemetry — pre-gate 적중률 + inconclusive-block 비율 측정verification-cascade.servicealways on
1pre-cascade 게이트 재정렬 (role/dummy/blacklist defense-in-depth)verify-email.tsdummy/blacklist on · role off
2정적 disposable 블록리스트 (vendored 5,438 도메인 + suffix match)email-validation.utiloff (shadow 후 flip)
3cascade single-flight + 30s total timeoutverification-cascade.serviceon
4MX 캐시를 Redis 로 이관 (24h+/1h-) + A/AAAA implicit-MX fallback + LRU front-cachemillion-verifier.serviceon
5circuit breaker 분산화 (Redis hydrate / async write-through)verification-cascade.serviceoff

캐시 정책 (Phase 4 적용 후)

레이어저장소TTL비고
검증 결과 캐시Redisev:<email>configMV 단발 시기와 동일
Cascade consensus 캐시Rediscascade-level 키provider 합의 결과 재사용 (재방문 시 0 quota)
MX 캐시Redis (Phase 4) + LRU front-cache도메인24h hit / 1h miss워커 재시작 콜드 해소
Circuit breaker 상태워커 in-process (Phase 5 미flip)provider분산화 deferred
Rate limitprovider 별 PQueue (in-process)distributed limiter deferred

YS Medi 인시던트 (2026-05-15, 사고)

Symptom. 캠페인이 자동으로 stopped + 모든 step skipped. DB 에 stop_reason='unreachable', stop_note='verification_cascade:all_tiers_inconclusive_or_unavailable'.

증거내용
HunterHTTP 429 "billing period limit 소진". 콘솔 11,877 / 10,000 (연간 quota, reset 2027-05-15)
FindymailHTTP 402 "Not enough credits" (verifier_credits: 0)
폭주 주범beta 워크스페이스 "YS Medi" 가 5/15 하루 20,242건 발송 (평소 1,000~1,500/일). 21,498건 중 고유 수신자 21,204 → 98.6% 캐시 미스 → cascade 거의 전부 풀 실행 → Findymail 소진 → Hunter 로 쏠려 연 한도를 하루에 소진
키 공유Hunter/Findymail/MV 키가 alpha · beta 공용 → 한도 합산 차감, 양쪽 모두 fail-CLOSED
valid 인데 막힘한 enrollment 가 07:39:01 stop → 3분 52초 뒤 같은 이메일이 다른 enrollment 에서도 동일 차단. quota 회복 전까지 valid 도 영구 차단

이 시기의 구조적 문제

  1. fail-CLOSED → 비가역 stop. provider 의 transient 장애(429/402)도 enrollment 영구 종료 사유로 격상.
  2. 비용 중복. buyer-search / on-demand enrich / CSV import 가 이미 upstream 에서 검증한 lead 인데 send-time 에 다시 cascade.
  3. cache miss 폭주에 약함. 신규 lead 가 한 번에 대량 enrol 되는 시나리오에서 consensus cache 가 무력화.
  4. API 키 공유. alpha/beta 환경 분리 없이 같은 quota 풀에서 차감.

③ 2026-05-21 이후 · cheap 게이트만 (현재)

개념

PR #7787 (이예인) 가 send-time cascade 호출 자체를 제거. 발송 직전에는 무료·결정론적 휴리스틱만 돌고, paid verification 책임은 buyer-search · on-demand enrichment · CSV import 의 upstream 으로 이동. 실패 처리는 다시 step-level skip으로 회귀 — enrollment 비가역 stop 분기 완전 제거.

호출 흐름

verifyAndDedup(toEmail)
  1. isValidEmailFormat            → failed
  2. roleBlock      (isUndeliverableEmail)   → skipped
  3. dummyBlock     (jane.doe/test/...)      → skipped
  4. disposableBlock (isDisposableDomain)    → skipped
  5. blacklistRedundant (Redis/DB)            → skipped
     // step 1.5 와 step 2 사이 ms-window 방어
  6. existingSend dedup                       → skipped
  → ok
  // paid cascade 호출 없음

제거 이유 (PR #7787 메시지 요약)

책임 이동

검증 시점주체대상실패 처리
buyer-searchlead-discovery cascadediscovery 결과row drop, lead 미생성
on-demand enrichmentlead-on-demand-enrich.worker.tsenrich 결과block-and-drop, lead 미persist
CSV importlead-import.service.ts업로드 배치MV batch 로 undeliverable/risky row 사전 reject
send-time (worker step 2)verifyAndDedup모든 outbound무료 게이트만. step-level skip.

캐시 정책 (현재)

레이어저장소TTL역할
Bounce blacklistRedis + DB fallbackemailstep 1.5 (resolve-lead) + step 2 (verify-email) 양쪽에서 조회
Bounce recordPostgres email_bouncesemailstep 1.5 의 보조 시그널
Disposable domainsvendored JSON도메인분기별 수동 갱신Phase 2 산물 — cascade 제거 후에도 잔존
Dedup indexPostgres emails unique on (sequence_id, step_id, to_email)같은 step 안 중복 발송 방지

잔존물 (legacy 흔적)

세대별 비교 — 한눈에

항목 ⓪ Hunter.io (12-30 ~ 03-09) ① MV 단발 (03-09 ~ 05-06) ② 3-tier cascade (05-06 ~ 05-21) ③ 현재 (05-21~)
도입자 Mohamed Magdy (#529/#532) Cheolhee Lee (#1978) Ahmed Mansy (#6594) 이예인 (#7787)
Paid provider Hunter.io 1개 MV 1개 MV → Findymail → Hunter 없음
Send-time API 호출 매번 1회 (캐시 없음) 최대 1회 (cache miss 시) 최대 3회 (tier escalation) 0회
DB 발송 이력 시그널 ✓ 30d 윈도우 ✓ (계승) (N/A — cascade 자체 없음)
MX 캐시 워커 in-process Map · 24h · 콜드 스타트 위험 Phase 4: Redis 이관 · 24h hit / 1h miss + LRU (cascade 제거로 미사용)
Result 캐시 Redis ev:<email> Redis + cascade consensus (cascade 제거로 미사용)
KR 도메인 보정 ✓ .kr/.한국 → risky 완화 ✓ (계승) (N/A)
API 장애 시 null (silent send-through 버그) Option B — MX-based fallback result provider escalation + circuit (N/A)
undeliverable 시 이메일 교체 ✓ 같은 도메인 다른 주소 swap (반송률↑ 원인) ✕ (PR #3163 에서 제거)
Format check 위치 cascade cascade cascade (Ahmed 가 옮김)
Role 차단 Phase 1 (default off) ✓ roleBlock flag
Disposable 차단 Phase 2 vendored 5,438 도메인 (default off) ✓ disposableBlock flag
Dummy 차단 inline 상수 (PR #3951) Phase 1 flag 화 ✓ dummyBlock flag
Blacklist 재검사 step 1.5 1회만 step 1.5 1회만 ✓ blacklistRedundant (1.5 + 2)
Single-flight ✕ (중복 호출 가능) Phase 3 (N/A)
Total timeout ✕ (워커 stall 가능) Phase 3 — 30s (N/A)
Circuit breaker Hunter 429 (chlee 2026-02-26 추가) MV credits-exhausted throw Phase 5 (default off, in-process) (N/A)
실패 시 enrollment step skip + email 교체 시도 step skip만 (lead-level dead-end 아님) stopped (unreachable, 비가역) step skip만
검증 책임 위치 send-time send-time send-time + upstream 일부 upstream (buyer-search · enrich · import)

캐시 정책의 변천 (논리적 정리)

왜 cascade 시기에 캐시가 다층화됐나

① 시기에는 캐시 미스 페널티가 API 호출 1회 + KR 도메인 보정 수준이라 단순했다. ② 시기에는 미스가 최악 3회 호출로 폭증하고, 각 provider 가 독립 quota / rate limit / circuit / consensus 를 가지면서 캐시도 그에 맞게 다층화됐다:

그런데 캐시 다층화가 인시던트를 막진 못했다. YS Medi 가 하루 20k 신규 수신자 (98.6% cache miss) 를 보낸 시점에 모든 캐시는 무용지물 — 그게 발송 패턴의 자연스러운 특성이라는 게 핵심. 캐시는 재방문을 싸게 하는 도구지, 신규 폭주를 막는 도구가 아니다.

현재(③) 의 캐시 단순화 의미

현재는 send-time cache 가 거의 사라졌다. 이유: 검증 자체가 send-time 에서 사라졌기 때문. 대신 upstream 의 검증 결과를 SSOT 인 DB(lead_contacts.is_verified) 에 영속시키고, send-time 은 그 신뢰를 그대로 받는 구조. 즉

이로 인해 send-time 의 cache pollution / single-flight / circuit-breaker / quota 관리 부담이 모두 사라졌다.

④ 제안 · MV 단발 복원 분석 + Hybrid (D)

질문: "phase 1 (MV 단발, 2026-03-09 ~ 2026-05-06) 의 형태로 발송워커에 다시 추가하는 게 맞나?"

결론: ① 시기 그대로 복원은 비추천. 그 시기에도 6주 뒤 #6947 가 정리한 약점이 있었고, 현재는 더 좋은 자산(Phase 4 Redis MX · disposable JSON · single-flight · SSOT)이 이미 있다. Hybrid (D) 가 최적.

① 시기 그대로 복원의 약점

약점 (#6947 회고)그대로 복원 시 영향
워커 재시작 시 in-process MX 캐시 손실DNS 폭주 — 재시작 후 첫 N분 모든 도메인 cold lookup
dummy/role 주소가 cache miss 때 MV 까지 진입무료로 막을 걸 quota 낭비
정적 disposable 블록리스트 없음10minutemail 류도 MV API 호출
MX → A/AAAA RFC 5321 fallback 누락MX 없는 정상 도메인 false-negative
동일 이메일 동시 검증 single-flight 부재같은 이메일 N개 enrollment 동시 처리 시 N회 중복 호출
API wallclock 무제한MV hang 시 워커 stall
SSOT 부재upstream 검증 결과(lead_contacts.is_verified)를 신뢰할 채널 없음 — 매 발송이 MV 진입 (그 시기엔 upstream 자체가 없었으니 자연스러움)

현재 갖춰진 자산 — ① 시기에는 없던 것

자산출처베타 상태 (2026-05-30 실측)
Phase 4 Redis MX 캐시 (24h hit / 1h miss + LRU)#6947mx:v1:* 396 키 활성 · enrich-side 호출이 채우는 중
Phase 2 disposable 블록리스트 (5,438 도메인)#6947code 잔존, default flag true
Phase 3 single-flight + 30s wallclock timeout#6947flag default on, verifyCascade.singleFlight=true
Cheap 게이트 4종 (role/dummy/disposable/blacklist)#6947 + #7787default true 운영 중 (verifyPreflight.*)
lead_contacts.is_verified SSOTenrichment 워커upstream 이 채우는 중
verifyAndDedup(_isContactVerified, _contactMetadata) legacy 인자PR #7760underscore 인자 그대로 — 무수정 wiring 자리
Format pre-check 위치 (cascade 앞)PR #6594이미 정리됨
bounce:bl:* blacklistbounce-check.service3,953 키 활성
cascade 인프라 (MV / Findymail / Hunter)PR #6594 + #6947전부 import 가능 · verifyEmail() export 유효

옵션 비교

A · ① 그대로 복원

호출 비용 중 · 안전성 중 · 사고 재발 낮 · bounce ↓

SSOT 미활용. 매 발송 MV 진입.

B · ② cascade 복원

호출 비용 매우 높 · 안전성 매우 낮

사고 재발 위험 매우 높음 — YS Medi 패턴.

C · ③ 현재 유지

호출 비용 0 · 안전성 높

upstream 검증 정확도에 100% 의존.

D · Hybrid 권장

호출 비용 저-중 · 안전성 매우 높 · 사고 재발 매우 낮

SSOT skip + ① 정신 + Phase 0~5 보강.

D — Hybrid 디자인 (4-Tier gate)

설계 원칙 3개:

verifyAndDedup(toEmail, isContactVerified, contactMetadata)

  ─── Tier 0 (free, instant) ───────────────────────  ← 이미 운영 중
   1. isValidEmailFormat        → failed
   2. roleBlock                 → skipped  (flag)
   3. dummyBlock                → skipped  (flag)
   4. disposableBlock           → skipped  (flag, vendored 5,438 도메인)
   5. blacklistRedundant        → skipped  (Redis bounce:bl:*)
   6. existingSend dedup        → skipped

  ─── Tier 1 (SSOT trust) ──────────────────────────  ← 신규 · PR #7760 분기 활용
   7. isContactVerified === true
      && !contactMetadata?.isLastResort
      && verifiedAt > now - VERIFY_TRUST_TTL_DAYS    (기본 30d)
      → MV 호출 SKIP, 발송 진행

  ─── Tier 2 (MV multi-signal, fail-OPEN) ──────────  ← ① 정신 + Phase 0~5 보강
   8. verifyEmail(toEmail)        // million-verifier.service
       ├─ Redis ev:cache                            (①)
       ├─ DB delivery/bounce history 30d            (①)
       ├─ Redis mx:v1: 24h/1h + A/AAAA fallback     (Phase 4)
       ├─ MV API + KR 도메인 보정 + Option B MX     (①)
       ├─ single-flight + 30s wallclock             (Phase 3)
       │
       ├─ CreditsExhausted / MV_AUTH_ERROR
       │   → fail-OPEN: 발송 진행 + 알람만
       ├─ result.undeliverable                       → step skip (enrollment 보존)
       └─ result.risky && score ≤ 20                 → step skip

  → ok → 발송

A (① 그대로) vs D (Hybrid) — 차이

항목A · ① 그대로D · Hybrid
Tier 1 SSOT skip✕ 모든 발송이 MV 진입✓ verified contact 는 호출 자체 안 함
MX 캐시 영속성in-process Map 24h (cold-start 위험)Redis 24h/1h + LRU + A/AAAA fallback
Disposable 사전 차단✕ MV 도달 후 차단✓ vendored 5,438 도메인
Single-flight✕ 중복 호출 가능✓ Phase 3
Total timeout✕ 워커 stall 가능✓ 30s
Stale 검증 정책(해당 없음)VERIFY_TRUST_TTL_DAYS 경과 시 재검증
환경별 API 키 분리(해당 없음)✓ alpha/beta 키 분리 (사고 직접 원인 회피)
Workspace quota(해당 없음)✓ 워크스페이스별 MV 호출 quota
Shadow / canary(해당 없음)✓ Phase 0 telemetry + workspace allowlist flag

운영 가드 (사고 회고 기반)

점진 도입 Roadmap

W0 PR — Tier 1 SSOT skip 분기만 추가 (Tier 2 OFF). Phase 0 shadow telemetry 활성 (verdict 로깅만, 실 분기 없음). W1 Shadow 로그 분석 — verified=true contact 의 실제 bounce 율 측정. 충분히 낮으면 send-time 호출 자체가 필요한지 재검토. (옵션 C 유지가 정답일 수 있음) W2 1개 워크스페이스 카나리 — Tier 2 activate (fail-OPEN). MV credit 소비 + bounce 율 변화 모니터링. W3 2~3개 워크스페이스로 확장. workspace quota 가드 검증. W4 alpha 전체 활성. 1주 모니터링 후 beta 까지.

중요한 자기 점검: W1 단계에서 verified=true contact 의 bounce 율이 이미 충분히 낮다면 Tier 2 자체를 추가하지 않는 게 정답이다. 즉 "MV 단발 복원" 은 가능 하지만 필요 한지가 데이터로 검증돼야 함. shadow 가 그 검증 도구.

예측 비용 (베타 실측 기반)

레퍼런스 (커밋 / PR)

날짜커밋작성자변경
2025-12-2776ece44e4 · 8a729c0a0 (PR #529)Mohamed Magdyhunterio-email-verifier.service 신규 — Hunter.io verify 서비스 도입
2025-12-30283cd0bb6 · 37f4055e2 (PR #532)Mohamed Magdy워커-내 검증 호출의 최초email-sequence-worker-v2.tsverifyEmail() + Hunter domain search fallback
2026-01-0213e5dce1b (PR #561)Cheolhee LeeBullMQ 마이그레이션 — sequence-email.worker.ts 로 이관. 검증 로직 따라옴
2026-02-265b37ed554Cheolhee LeeHunter.io 429 circuit breaker 도입
2026-03-034d33fae20CheolheeLee0시퀀스 이메일 발송 전 이메일 포맷 pre-validation 추가
2026-03-099b866b960 (PR #1976)Cheolhee LeeMillionVerifier API key config 추가
2026-03-0906e3c47ce (PR #1978)Cheolhee LeeHunter.io → MillionVerifier multi-signal pipeline 전면 교체 (① 단계 시작점). DB history + MX + KR 보정 + Option B fallback + null-as-format-error 픽스
2026-03-25aba15ded7 (PR #3021)Cheolhee LeeMV 크레딧 고갈 시 에러 전파 — 워커 무한 fallback 방지
2026-03-26a3b264b92 (PR #3163)Cheolhee Leeenrichment 이메일 교체 로직 제거 — 반송률 55.5% 원인 차단 (⓪ 시기 도메인-스왑 fallback 종료)
2026-03-31e8fb6b400Cheolhee Leerefactor — sequence-email worker 1641줄 → 12개 모듈 분리. steps/verify-email.ts 가 이 시점에 생김
2026-04-06e32527ae5 (PR #3951)Cheolhee Leerisky 저점수 이메일 + DUMMY_PATTERNS 발송 사전 차단
2026-04-090bc3abc7ejaykim-cmdrisky threshold 30 → 20
2026-04-2255943b980Cheolhee Leeskipped/failed 종료 경로 정리
2026-05-068abafd828 (PR #6594)Ahmed Mansy3-tier cascade 도입 + fail-CLOSED stop (② 단계 시작점)
2026-05-08355774476 (PR #6947)Hojin KangPhase 0~5 신뢰성 강화
2026-05-21f99670d4f (PR #7760)lsuminlverified contact send-time skip 분기
2026-05-218485f96b3 (PR #7787)이예인cascade 호출 자체 제거 (③ 단계 — 현재)