Skip to content

Dead Letter Topics — Trigger Conditions & Processing

Dead Letter Topic (DLT) là cơ chế xử lý message không thể acknowledge được sau nhiều lần retry. Không có DLT, message sẽ bounce giữa các subscribers cho đến khi retention period hết — chiếm tài nguyên, tạo noise trong monitoring, và không có cách systematic nào xử lý. DLT giải quyết vấn đề này bằng cách tách "poison messages" ra khỏi main flow.

Tại sao cần Dead Letter Topic

Không phải mọi message failure đều có thể retry được. Có hai loại failure cơ bản:

Transient failure: Database tạm thời không available, memory pressure, downstream service timeout. Retry sẽ thành công sau một khoảng thời gian.

Permanent failure: Message có format sai, schema không match, business logic validation fail, dependent entity không tồn tại. Retry mãi cũng không thành công.

Nếu không có DLT, permanent failure dẫn đến:

  1. Message bị redeliver liên tục
  2. Subscriber lãng phí CPU/memory xử lý message không thể ack
  3. Các "poison messages" này chiếm delivery slot, tăng num_undelivered_messages
  4. NACK storms có thể ảnh hưởng throughput của toàn subscription

DLT cung cấp một "escape hatch": sau N lần thử, message được chuyển sang topic khác để xử lý riêng (inspect, retry thủ công, alert, log để phân tích).

Trigger condition — max delivery attempts

DLT trigger khi một message đạt max delivery attempts threshold. Đây là số lần Pub/Sub attempt deliver message, tính từ lần đầu tiên.

Configuration range: 5 đến 100 delivery attempts. Default: 5.

yaml
# Ví dụ tạo subscription với DLT
gcloud pubsub subscriptions create my-subscription \
  --topic=my-topic \
  --dead-letter-topic=my-dead-letter-topic \
  --max-delivery-attempts=10

"Approximate" — điều cần biết

Docs nói rõ: "The maximum number of delivery attempts is approximate because Pub/Sub forwards undeliverable messages on a best-effort basis."

Điều này có nghĩa gì trong thực tế? Pub/Sub không guarantee chính xác rằng sau đúng 10 attempts, message được forward. Message có thể được forward ở attempt 9 hoặc 11 trong một số trường hợp. Đây là do:

  • Distributed nature của Pub/Sub: delivery state được track trên nhiều servers
  • Eventual consistency trong việc update delivery count
  • Race conditions giữa delivery và count update

Practical implication: Đừng thiết kế business logic dựa vào "đúng N lần retry thì xảy ra X". DLT là safety net cho "message không xử lý được", không phải mechanism để count retries chính xác.

Phân biệt "delivery attempts" và "processing attempts"

Đây là điểm gây nhầm lẫn quan trọng. Delivery attempt là mỗi lần Pub/Sub deliver message tới subscriber. Processing attempt là số lần subscriber code chạy với message đó.

Nếu subscriber nhận message, xử lý, nhưng crash trước khi ack → ack deadline expire → Pub/Sub redeliver. Đây là delivery attempt thứ 2, nhưng processing attempt đã là thứ 1 (và mất dữ liệu trung gian của lần đó).

Nếu subscriber nhận message nhưng extend ack deadline nhiều lần rồi mới ack → vẫn là delivery attempt thứ 1 (message chỉ delivered một lần, subscriber đang hold lease).

Điều này có nghĩa: nếu subscriber NACK ngay lập tức mà không retry internally, 5 NACKs = 5 delivery attempts = DLT trigger. Nếu subscriber retry internally (nhận message, xử lý 3 lần, rồi NACK), DLT trigger sau 5 delivery attempts × 3 internal retries = 15 processing attempts.

Cơ chế forward message sang DLT

Khi DLT trigger, Pub/Sub không chỉ đơn thuần republish message gốc vào DLT topic. Nó wrap message gốc vào một message mới với metadata attributes:

json
{
  "data": "<original-message-data (không đổi)>",
  "attributes": {
    "CloudPubSubDeadLetterSourceDeliveryCount": "10",
    "CloudPubSubDeadLetterSourceSubscription": "projects/my-project/subscriptions/my-sub",
    "CloudPubSubDeadLetterSourceSubscriptionProject": "my-project",
    "CloudPubSubDeadLetterSourceTopicPublishTime": "2025-01-15T10:00:00Z",
    "CloudPubSubDeadLetterSourceDeliveryErrorMessage": "..."
  }
}

Các attributes quan trọng:

  • CloudPubSubDeadLetterSourceDeliveryCount: Số lần delivery attempt khi forward
  • CloudPubSubDeadLetterSourceSubscription: Subscription gốc (để trace back)
  • CloudPubSubDeadLetterSourceTopicPublishTime: Thời điểm message được publish ban đầu
  • CloudPubSubDeadLetterSourceDeliveryErrorMessage: Lý do failure (nếu có)

Message data không thay đổi: Payload gốc của message được giữ nguyên. Consumer của DLT nhận được chính xác data như subscriber gốc sẽ nhận.

Message attributes gốc được giữ nguyên: Nếu message gốc có attributes, chúng được merge với DLT attributes. Nếu có conflict (cùng key), DLT attributes override.

IAM requirements — dễ bị quên

Đây là một trong những lý do phổ biến nhất khiến DLT không hoạt động sau khi cấu hình:

Pub/Sub service account của project cần có quyền publish vào DLT topic. Pub/Sub không dùng identity của bạn để forward — nó dùng service account của chính Pub/Sub service.

bash
# Lấy service account của Pub/Sub trong project
PROJECT_NUMBER=$(gcloud projects describe my-project --format='value(projectNumber)')
PUBSUB_SA="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"

# Grant publisher role trên DLT topic
gcloud pubsub topics add-iam-policy-binding my-dead-letter-topic \
  --member="serviceAccount:${PUBSUB_SA}" \
  --role="roles/pubsub.publisher"

# Subscriber cũng cần Subscriber role trên source subscription
gcloud pubsub subscriptions add-iam-policy-binding my-subscription \
  --member="serviceAccount:${PUBSUB_SA}" \
  --role="roles/pubsub.subscriber"

Nếu thiếu IAM permission, DLT config vẫn được accepted khi tạo subscription, nhưng khi DLT trigger, forward sẽ fail silently. Message sẽ bị stuck (tiếp tục redeliver, delivery count không tăng nữa theo một số behaviors). Đây là bug khó debug vì không có explicit error trong Pub/Sub UI.

Luôn verify IAM trước khi test DLT bằng cách kiểm tra policy binding.

DLT subscription và processing

DLT topic cần ít nhất một subscription để messages không bị bỏ qua. Nếu không có subscription, messages vào DLT topic và bị xóa sau retention period mà không ai xử lý.

Retention policy của DLT subscription

Message trong DLT subscription sử dụng retention policy của DLT subscription, không của source subscription. Điều này quan trọng vì:

  • Source subscription có thể có retention 7 ngày
  • Nhưng DLT subscription bạn có thể muốn giữ lâu hơn (30 ngày) để có thời gian investigate

Patterns xử lý dead letters

Pattern 1: Alert + Manual inspection

Đây là pattern phổ biến nhất. DLT subscription trigger alert khi có messages mới. Team on-call inspect messages, xác định root cause, xử lý thủ công (hoặc fix code và replay).

python
# Subscriber đơn giản cho DLT — log và alert
def process_dead_letter(message):
    delivery_count = message.attributes.get('CloudPubSubDeadLetterSourceDeliveryCount')
    source_sub = message.attributes.get('CloudPubSubDeadLetterSourceSubscription')
    
    logger.error(f"Dead letter received: delivery_count={delivery_count}, source={source_sub}")
    send_alert(message)  # PagerDuty, Slack, etc.
    
    message.ack()  # Luôn ack trong DLT subscriber

Pattern 2: Retry với exponential backoff

DLT subscriber đọc message, chờ một khoảng thời gian, rồi republish vào source topic. Cẩn thận: nếu message vĩnh viễn broken, nó sẽ loop giữa source topic và DLT mãi.

python
# Thêm "DLT retry count" vào attributes để track loop
def process_dead_letter(message):
    retry_count = int(message.attributes.get('dlt_retry_count', 0))
    
    if retry_count >= 3:
        # Lưu vào BigQuery/Cloud Storage để phân tích
        archive_message(message)
        message.ack()
        return
    
    # Republish với retry count tăng
    publisher.publish(
        source_topic,
        data=message.data,
        **{**message.attributes, 'dlt_retry_count': str(retry_count + 1)}
    )
    message.ack()

Pattern 3: Archive to BigQuery

Mọi dead letter được lưu vào BigQuery để phân tích offline. Hữu ích khi cần phân tích pattern của failures.

Monitoring DLT

Metrics quan trọng:

  • topic/send_message_operation_count trên DLT topic: số messages được forward sang DLT
  • subscription/num_undelivered_messages trên DLT subscription: backlog của DLT (nếu DLT subscriber chậm)

Alert quan trọng: Alert ngay khi DLT nhận bất kỳ message nào. DLT là signal "có gì đó sai" trong pipeline, không nên để âm thầm.

yaml
# Cloud Monitoring alert policy
alertPolicy:
  conditions:
  - conditionThreshold:
      filter: 'resource.type="pubsub_topic" AND
               resource.labels.topic_id="my-dead-letter-topic" AND
               metric.type="pubsub.googleapis.com/topic/send_message_operation_count"'
      comparison: COMPARISON_GT
      thresholdValue: 0
      duration: 0s

Common mistakes

Không set IAM permissions: DLT được cấu hình nhưng không có publisher permission → messages không được forward, không có error, DLT trông như hoạt động nhưng không nhận gì.

Không có subscription trên DLT topic: Messages forward vào DLT nhưng không có ai đọc → messages expire sau retention, silent loss.

Quá thấp max delivery attempts: 5 attempts có thể không đủ nếu downstream service recover chậm sau incident. Messages hợp lệ có thể land vào DLT chỉ vì timing.

Quá cao max delivery attempts: 100 attempts với message 1KB, 1 subscription → tốn tài nguyên redeliver trong nhiều ngày trước khi DLT trigger.

DLT loop: DLT subscriber republish vào source topic mà không có circuit breaker → infinite loop nếu message permanently broken.

References