ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Cloudflare Queues][운영] explicit ack를 켠 consumer에서 poison message 격리와 DLQ 이동을 어떤 기준으로 먼저 나누나
    기타개발지식/풀스택개발 2026. 8. 10. 20:16

    IT 리서치 노트

    [Cloudflare Queues][운영] explicit ack를 켠 consumer에서 poison message 격리와 DLQ 이동을 어떤 기준으로 먼저 나누나

    Cloudflare Queues consumer에서 explicit ack를 켜기 시작하면 같은 batch 안에서도 성공 메시지, 잠깐 실패한 메시지, 독성 payload를 서로 다른 방식으로 처리할 수 있다. 2026년 8월 10일 기준 Cloudflare 공식 문서를 다시 보면 explicit ack는 이후 batch 실패가 와도 이미 처리한 메시지를 재전달 대상에서 빼 주고, 개별 retry는 즉시 다시 queue로 넣을 수 있으며, DLQ는 재시도 한도 이후의 보존 경로다. 이 글은 explicit ack consumer에서 poison message 격리와 DLQ 이동을 어떤 기준으로 먼저 나눠야 하는지 정리한 것이다.

    1. 개요

    결론부터 말하면 explicit ack consumer에서는 성공 메시지 ack, 일시 실패 retry, poison 후보 메모, DLQ 최종 보존 순서로 보는 편이 맞다. 독성 payload를 만났다고 batch 전체를 같은 규칙으로 태우면 explicit ack의 장점을 거의 잃는다.

    즉 triage의 첫 질문은 "이 메시지를 다시 봐야 하나"가 아니라 "이 메시지는 이미 성공으로 확정됐나"다. 성공 메시지를 먼저 분리해야 poison 분석과 backlog 운영이 짧아진다.

    2. 어디서 실제로 막히는가

    실무에서 자주 꼬이는 지점은 세 가지다. 첫째, explicit ack를 켰는데도 성공 메시지를 끝까지 pending 상태로 두고 batch 전체 판단을 기다린다. 둘째, 외부 API 429와 payload 검증 오류를 모두 retry로 묶는다. 셋째, DLQ를 poison 격리의 시작점으로 오해해 consumer 안의 분기 기준이 비어 버린다.

    Cloudflare 문서는 explicit ack가 성공 메시지를 재전달 대상에서 빼 준다고 설명하고, pull consumer 문서는 명시적 retry가 해당 메시지를 곧바로 다시 queue에 넣는다고 적는다. DLQ 문서는 재시도 한도에 닿은 뒤 별도 보존 queue로 보낼 수 있다고 설명한다. 이 셋을 합치면 consumer 안에서 먼저 나눠야 할 것은 retry 숫자가 아니라 메시지 종류다.

    • 증상: 독성 payload 한 건 때문에 정상 메시지까지 다시 돈다.
    • 실패: explicit ack를 켰는데도 성공 건을 바로 떼지 않는다.
    • 막힘: retry 가치가 있는 실패와 poison 실패를 같은 버킷으로 본다.
    • 누락: DLQ에 가기 전 poison 후보 메모가 없다.
    상황 먼저 볼 곳 판단 기준
    정상 메시지가 다시 보인다 ack 시점 성공 직후 ack됐는지 본다
    같은 실패가 계속 돈다 retry 사유와 fingerprint 일시 실패인지 payload 독성인지 나눈다
    중요 메시지를 버리기 싫다 DLQ 설정 최종 보존 경로가 있는지 본다

    3. 실무에서 적용하는 순서

    실무에서는 다섯 단계가 가장 짧다. 먼저 성공 메시지를 즉시 ack한다. 두 번째로 외부 429, timeout 같은 일시 실패만 retry로 남긴다. 세 번째로 schema mismatch, 필수 필드 누락, 복구 불가 payload는 poison 후보로 따로 메모한다. 네 번째로 같은 poison이 반복되면 fingerprint 기준으로 격리 대상을 확정한다. 마지막으로 재시도 한도에 닿은 메시지는 DLQ에서 분석과 수동 재처리로 넘긴다.

    1. 성공 메시지는 handler 안에서 바로 ack한다.
    2. retry는 일시적 실패에만 쓴다.
    3. 복구 불가 payload는 poison 후보로 기록한다.
    4. 같은 fingerprint가 반복되면 격리 기준을 확정한다.
    5. 최종 보존은 DLQ에서 처리한다.

    실제 운영에서는 Cloudflare dashboard 메뉴에서 queue 상세 탭을 열고, consumer 로그 화면에서 message id를 조회하고, disposition 필드를 확인하고, retry 설정을 저장한 뒤, 결과 상태와 오류 응답을 다시 확인하는 순서가 가장 짧다. CLI를 실행할 때도 같은 필드 이름을 메모에 그대로 복사해 두면 ack, retry, poison_candidate를 나중에 비교하기 쉽다.

    운영 메모를 남길 때는 queue 메뉴 경로, consumer 탭 이름, message id 필드, disposition 필드, retry 버튼을 어디에서 확인했는지 같이 적는 편이 좋다. 같은 incident 안에서 로그 조회, 결과 비교, 오류 확인, 설정 저장, 재실행 순서를 남겨 두면 다음 교대자도 콘솔과 대시보드에서 같은 경로로 바로 따라간다.

    message_id=...
    fingerprint=sha256:...
    disposition=ack|retry|poison_candidate
    retry_reason=upstream_timeout
    poison_reason=schema_mismatch
    dlq_ready=true|false

    이 순서를 잡아 두면 poison incident와 backlog incident를 같은 runbook에 우겨 넣지 않게 된다. retry 가치가 있는 메시지와 다시 태울수록 손해인 메시지가 분리되기 때문이다.

    4. 공식 문서와 예시 화면으로 확인하기

    아래 자료를 볼 때는 각 화면에서 어떤 메뉴, 버튼, 필드, 상태 문구를 확인해야 하는지 같이 읽어야 한다. 단순히 캡처를 보는 것보다 ack 결과, retry 결과, DLQ 상태를 어디에서 조회하는지 먼저 짚어 두는 편이 낫다.

    첫 공식 화면은 explicit acknowledgement 구간이다. Cloudflare는 batch 안에서 이미 처리한 메시지를 개별 ack로 먼저 빼낼 수 있다고 적어 두었다.

    explicit ack를 쓰면 이후 메시지 실패가 생겨도 이미 ack한 메시지는 다시 전달되지 않는다.
    explicit ack를 쓰면 이후 메시지 실패가 생겨도 이미 ack한 메시지는 다시 전달되지 않는다.

    즉 poison message를 만났다고 해서 앞에서 성공한 메시지까지 같은 fate로 묶을 필요가 없다. 먼저 성공 메시지를 떼어 두는 것이 triage의 시작점이다.

    두 번째 자료는 메시지별 retry 설명이다. 일시적 외부 API 장애처럼 재시도 가치가 있는 메시지는 DLQ보다 retry로 먼저 분기하는 편이 맞다.

    Cloudflare는 명시적 retry가 해당 메시지를 곧바로 다시 queue에 넣는다고 설명한다.
    Cloudflare는 명시적 retry가 해당 메시지를 곧바로 다시 queue에 넣는다고 설명한다.

    이 문구 덕분에 retry와 poison 격리를 한 줄로 쓰면 안 된다는 점이 더 분명해진다. retry는 복구 기대가 있는 실패이고, poison은 격리 후보다.

    세 번째 공식 화면은 DLQ다. 재시도 한도에 닿은 메시지를 버리지 않고 따로 보존할 수 있다는 기준을 여기서 고정해야 한다.

    DLQ는 재시도 한도에 닿은 메시지를 별도 queue에 보존하는 장치다.
    DLQ는 재시도 한도에 닿은 메시지를 별도 queue에 보존하는 장치다.

    중요한 점은 DLQ가 '즉시 격리 버튼'이 아니라 최종 보존 경로라는 것이다. 따라서 소비자 코드에서 어떤 실패를 retry로 남길지, 어떤 실패를 격리 후보로 메모할지 먼저 나눠야 한다.

    운영에서는 batch 실패를 메서드가 아니라 실패 종류로 먼저 나누는 편이 빠르다. explicit ack를 켠 consumer에서 성공, 일시 실패, 독성 payload, 재시도 소진 후 보존을 한 표로 묶었다.

    explicit ack consumer에서 poison message와 DLQ 이동을 나누는 기준표다.
    explicit ack consumer에서 poison message와 DLQ 이동을 나누는 기준표다.

    이미 ack·retry·retryAll 분기 글과 poison fingerprint 글을 읽었다면, 이번 표는 explicit ack consumer라는 전제를 얹은 후속편이다.

    마지막 자료는 consumer 패턴 예시다. 핵심은 성공 메시지를 먼저 ack하고, retry와 poison 메모를 같은 trace에 남긴 뒤, DLQ는 최종 보존 경로로 읽는 것이다.

    explicit ack consumer에서 성공·재시도·poison 후보를 나누는 예시다.
    explicit ack consumer에서 성공·재시도·poison 후보를 나누는 예시다.

    이 구조를 잡아 두면 retryAll 뒤 backlog와 retention을 보는 글과도 연결된다. 즉 batch 전체를 다시 태울지보다, 어떤 메시지를 이미 빼냈는지가 더 중요한 장면이 생긴다.

    5. 주의사항과 리스크

    첫 번째 리스크는 explicit ack를 켜 놓고도 성공 메시지를 바로 떼지 않는 것이다. 두 번째는 poison 후보를 retry만으로 끌어 같은 실패를 여러 번 태우는 것이다. 세 번째는 DLQ가 있다는 이유로 consumer 안의 실패 분류를 느슨하게 두는 것이다.

    특히 at-least-once 전달 환경에서는 "한 번 더 보일 수 있다"는 사실과 "같은 독성 payload를 계속 태운다"는 사실을 구분해야 한다. 운영 메모에는 어떤 메시지를 ack했고, 어떤 메시지를 retry했고, 어떤 fingerprint를 poison 후보로 올렸는지를 같이 적는 편이 좋다.

    • explicit ack의 핵심은 성공 메시지 분리다.
    • retry는 복구 기대가 있을 때만 의미가 있다.
    • DLQ는 최종 보존 경로이지 첫 분기점이 아니다.

    6. 결론

    explicit ack consumer에서 poison message를 잘 다루려면 먼저 성공 메시지를 빼내고, 그다음 retry 가치가 있는 실패와 격리해야 할 실패를 나눠야 한다. DLQ는 그 마지막 보존 경로다. 실제 재주입 전에 무엇을 다시 확인할지까지 이어 보고 싶다면 DLQ replay 체크리스트 후속 글에서 idempotency key, batch disposition, lease_id, backlog 기준선 순서를 함께 보면 된다.

    • 성공 메시지는 바로 ack한다.
    • 일시 실패와 poison 실패를 다른 메모로 남긴다.
    • 재시도 한도 이후의 보존은 DLQ로 넘긴다.

    7. 참고 링크

    1. https://developers.cloudflare.com/queues/configuration/batching-retries/
    2. https://developers.cloudflare.com/queues/configuration/pull-consumers/
    3. https://developers.cloudflare.com/queues/configuration/dead-letter-queues/
    4. https://developers.cloudflare.com/queues/reference/delivery-guarantees/
Designed by Tistory.