팀에서는 dead letter exchange를 “실패 메시지가 무조건 모이는 안전한 통”처럼 말하는 경우가 많지만, 실제 DLX 장애는 한 단계 더 앞에서 시작됩니다. 첫 질문은 “메시지가 어디로 갔지?”가 아니라 “왜 원래 queue를 떠났지?”여야 합니다. 실제로 운영 중이던 알림 시스템에서 “메시지 유실” 장애가 터졌는데, 팀 전원이 DLX 바인딩만 들여다보고 있었습니다. 알고 보니 원인은 consumer가 파싱 실패 시 requeue=false로 nack하고 있었고, DLX target exchange는 이름만 같고 바인딩이 없는 빈 exchange였습니다. x-death 헤더를 보니 reason이 rejected로 찍혀 있어서 5분 만에 원인을 좁힐 수 있었습니다. 이 분기를 건너뛰면 TTL 문제를 nack 문제처럼 보고, policy 문제를 binding 문제처럼 디버깅하게 됩니다.
짧게 말하면 먼저 dead-letter trigger를 확인하고, 그다음 어떤 config source가 queue를 지배하는지 확인한 뒤, 마지막으로 DLX 라우팅 경로를 봐야 합니다.
이 글이 맞는 상황
아래 중 하나면 여기서 시작하면 됩니다.
- reject되거나 expire된 메시지가 기대한 queue로 안 간다
- source queue에서는 메시지가 빠지는데 dead-letter queue는 비어 있다
- 환경마다 DLX 동작이 다르다
- policy가 적용된 건지
x-arguments가 적용된 건지 헷갈린다 - dead-letter 이후 메시지가 사라진 것처럼 보인다
처음 10분에 볼 것
1차 확인은 이 정도면 충분합니다.
rabbitmqctl list_queues name arguments policy messages_ready messages_unacknowledged
rabbitmqctl list_bindings source_name destination_name routing_key
rabbitmqctl list_exchanges name type
rabbitmqctl list_policies
이 단계에서 답해야 할 질문은 네 개입니다.
- 정확히 어떤 이벤트 때문에 dead-letter가 발생했는가
- queue를 policy가 제어하는가,
x-arguments가 제어하는가, 둘 다인가 - republish될 때 어떤 routing key가 쓰이는가
- DLX target exchange와 binding이 실제로 존재하는가
dead lettering은 하나의 이벤트가 아니라 네 가지 trigger입니다
RabbitMQ dead-lettering 문서는 queue 수준에서 네 가지 trigger를 설명합니다.
- consumer가
requeue=false로 reject 또는 nack했다 - per-message TTL이 만료됐다
- queue length limit 때문에 메시지가 밀려났다
- quorum queue에서 delivery limit를 초과했다
즉 “DLX로 갔다”만으로는 정보가 부족합니다. trigger를 알아야 다음에 볼 층위가 결정됩니다.
한 가지 edge case도 중요합니다. queue 전체가 expire되는 경우 RabbitMQ는 그 안의 메시지를 dead-letter하지 않습니다.
trigger와 route를 먼저 분리해야 합니다
이 분리만 해도 시간 낭비가 많이 줄어듭니다.
| 먼저 일어난 일 | 보통 의미 | 다음 확인 포인트 |
|---|---|---|
basic.reject 또는 basic.nack with requeue=false | consumer 쪽 판단 | handler logic과 ack path 확인 |
| message TTL 만료 | timing과 retention 문제 | per-message TTL과 queue flow 확인 |
| queue length limit 초과 | backlog 또는 limit 정책 문제 | limits, overflow, drain rate 확인 |
| quorum delivery limit 초과 | 반복 requeue 또는 poison message 패턴 | x-delivery-count, delivery-limit 확인 |
어느 갈래인지 모르면 DLX binding을 만지는 건 대개 너무 이릅니다.
policy를 우선해야 하지만 x-arguments가 override합니다
RabbitMQ 문서는 hardcoded queue x-arguments보다 policy를 권장합니다. policy는 애플리케이션 재배포 없이 바꿀 수 있기 때문입니다. 하지만 운영상 중요한 함정이 하나 있습니다. 둘 다 있으면 queue arguments가 policy를 override합니다.
그래서 DLX 장애는 이런 식으로 헷갈립니다.
- 운영자는 policy를 바꿨다고 생각한다
- 애플리케이션은
x-dead-letter-exchange를 직접 선언했다 - queue는 계속 hardcoded argument를 따른다
팀이 policy를 보고 있는데 실제 queue는 arguments에 묶여 있다면, 조사 방향이 시작부터 틀어집니다.
routing key 가정이 틀려서 route가 어긋나는 경우가 많습니다
RabbitMQ는 dead-lettered message를 두 방식 중 하나로 다시 publish합니다.
- queue에 dead-letter routing key가 설정되어 있으면 그 key 사용
- 없으면 원래 publish 경로의 routing key 사용
즉 DLX 자체는 살아 있어도 팀이 상상한 routing key와 실제 key가 달라서 원하는 dead-letter queue로 못 가는 일이 충분히 생깁니다.
target exchange가 없으면 특히 위험합니다
RabbitMQ 문서는 DLX target exchange가 queue declaration 시점에 존재할 필요는 없지만, 실제 dead-letter 시점에는 반드시 있어야 한다고 설명합니다. 그 순간 exchange가 없으면 메시지는 조용히 drop됩니다.
실제 장애에서는 이걸 제일 먼저 확인하는 편이 좋습니다.
권한도 DLX 설정의 일부입니다
queue를 dead-letter exchange와 함께 선언할 때 RabbitMQ는 그 시점에 권한도 확인합니다.
- queue에 대한 configure 권한
- source queue에 대한 read 권한
- target DLX에 대한 write 권한
즉 topology는 맞아 보여도, 선언한 사용자의 권한이 부족해서 훨씬 앞단에서 문제가 생길 수 있습니다.
dead-letter cycle은 실제로 생기고, RabbitMQ는 메시지를 drop합니다
RabbitMQ 문서는 dead-letter된 메시지가 결국 같은 queue로 다시 돌아오는 cycle을 설명합니다. 무한 루프를 막기 위해 RabbitMQ는 cycle을 감지하고, 그 cycle 안에 rejection이 한 번도 없었다면 메시지를 drop합니다.
보통 이런 조건에서 잘 보입니다.
- default exchange를 가볍게 쓴다
- explicit dead-letter routing key가 없다
- 경로가 실수로 원래 queue로 되돌아간다
기본 DLX republishing은 강한 안전 보장이 아닙니다
RabbitMQ에서 dead-lettering도 결국 publish의 한 형태이고, publish는 실패할 수 있습니다. 공식 문서는 기본 dead-letter republishing이 내부적으로 publisher confirms 없이 이뤄진다고 설명합니다. 그래서 clustered DLX 경로가 항상 안전하다고 보면 안 됩니다.
즉 메시지는 source queue를 떠났지만 target 쪽이 못 받으면 그대로 잃을 수도 있습니다.
quorum queue에는 at-least-once dead-lettering이라는 더 안전한 선택지가 있지만, 그건 quorum 전용 기능이고 별도의 tradeoff가 있습니다.
x-death는 추측을 멈추게 해 주는 가장 빠른 단서입니다
RabbitMQ는 dead-letter history를 header에 남깁니다.
- AMQP 0-9-1에서는
x-death - RabbitMQ는
x-first-death-*,x-last-death-*도 추가
dead-letter reason 값에는 아래가 포함됩니다.
rejectedexpiredmaxlendelivery_limit
즉 메시지가 왜 이동했는지 굳이 추측할 필요가 없을 때가 많습니다. broker가 이미 기록해 둡니다.
dead-lettering은 message metadata도 바꿉니다
RabbitMQ 문서는 dead-lettering 과정에서 일부 metadata가 바뀐다고 설명합니다.
- exchange 이름은 최신 dead-letter exchange로 바뀐다
- routing key는 configured dead-letter routing key로 교체될 수 있다
- original TTL은 다음 queue에서 즉시 다시 만료되지 않도록 제거된다
그래서 dead-lettered message만 보고 원래 publish path를 복원할 때는, broker가 이미 일부 값을 다시 썼다는 점을 감안해야 합니다.
흔한 원인
1. trigger를 잘못 가정함
실제 원인은 TTL, queue length, quorum delivery limit인데 팀은 routing만 보고 있습니다.
2. policy 대 x-arguments drift
운영자는 policy를 바꿨지만 queue는 여전히 hardcoded argument를 따릅니다.
3. routing key 가정이 틀림
DLX는 있는데 팀이 예상한 key와 다르게 republish됩니다.
4. DLX target exchange 또는 binding 부재
메시지는 source queue를 떠나지만 그다음에 사라집니다.
5. dead-letter cycle
메시지가 같은 queue 경로로 되돌아오고 결국 drop됩니다.
흔한 잘못된 시작
- trigger를 확인하기 전에 DLX binding부터 보기
- queue arguments를 안 보고 policy가 먹고 있다고 가정하기
- DLX를 모든 cluster 상황에서 안전한 sink로 생각하기
x-death를 안 보고 기억에 의존해 원인을 복원하기- target exchange 존재 여부를 안 본 채 consumer부터 바꾸기
실전 점검 순서
1. dead-letter trigger를 먼저 식별합니다
메시지가 왜 source queue를 떠났는지 모르면 나머지는 거의 추측입니다.
2. policy가 지배하는지 x-arguments가 지배하는지 확인합니다
진짜 source of truth가 어디 있는지 먼저 알아야 합니다.
3. DLX exchange, binding, routing-key 경로를 확인합니다
trigger와 control plane이 명확해진 다음에야 route를 봐야 합니다.
4. x-death와 first/last death header를 확인합니다
RabbitMQ가 이미 reason과 경로 히스토리를 남겨 두었습니다.
5. cycle, missing target, cluster safety 가정을 확인합니다
메시지가 사라지는 사건은 여기서 설명되는 경우가 많습니다.
체크리스트
- 정확한 dead-letter trigger를 확인했다
- policy와
x-arguments중 누가 queue를 지배하는지 확인했다 - dead-letter routing key 가정을 검증했다
- DLX target exchange와 binding 존재를 확인했다
- topology를 바꾸기 전에
x-death를 확인했다
FAQ
Q. DLX를 붙이면 실패 메시지는 항상 보존되나요?
아니요. 기본 dead-letter republishing은 end-to-end 안전 보장과 다릅니다.
Q. policy를 바꿨는데 왜 queue 동작이 안 바뀌죠?
queue x-arguments가 DLX policy를 override하고 있을 수 있습니다.
Q. source queue에서는 메시지가 빠지는데 dead-letter queue는 왜 비어 있나요?
target exchange가 없거나, binding이 틀렸거나, routing key 가정이 다를 수 있습니다.
Q. 메시지가 왜 dead-letter됐는지 가장 빨리 확인하는 방법은 뭔가요?
x-death reason과 first/last death header를 보는 겁니다.
Read Next
- 반복 requeue 패턴이 quorum queue에서 시작됐다면 RabbitMQ Quorum Queues를 이어서 보세요.
Related Posts
Sources:
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis, RabbitMQ, Kafka 중 어디부터 볼까 Redis, RabbitMQ, Kafka가 함께 있는 시스템에서 지금 보이는 장애가 어느 계층에 더 가까운지, 첫 10분 안에 무엇을 확인하고 어떤 글로 들어가야 하는지 정리한 실전 허브 가이드입니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Astro 기술 블로그 SEO 체크리스트: 트래픽 기다리기 전에 먼저 고칠 것 Astro 기술 블로그를 위한 실전 SEO 체크리스트입니다. 배포 호스트 확인, robots.txt, sitemap, canonical, hreflang, 구조화 데이터, 페이지별 메타데이터, noindex 판단, 검증 명령까지 우선순위대로 정리합니다.
- 다국어 블로그 canonical과 hreflang 설정 가이드: 무엇을 확인하고 어디서 깨질까 다국어 블로그에서 canonical과 hreflang을 어떻게 설정해야 하는지 실전 기준으로 정리합니다. self-canonical, 상호 연결되는 hreflang 묶음, x-default, 카테고리 페이지, 최종 렌더 HTML 점검, 한 언어 버전이 다른 언어 버전을 눌러버리는 실수까지 다룹니다.
- OpenAI Codex CLI 설치 가이드: 설치, 인증, 첫 작업까지 OpenAI Codex CLI를 실전 기준으로 설치하는 방법을 정리했다. 설치, 로그인, 첫 실행, Windows 주의점, 첫 작업을 어떻게 시작하면 좋은지까지 다룬다.