Dead Letter Queues: Het afhandelen van 'poison messages' in gedistribueerde systemen

Voorkom dat gifberichten wachtrijen blokkeren

Inhoud

Een dead-letter queue (DLQ) is het vangnet dat berichten vangt die je consumers niet kunnen verwerken, zodat één defecte payload niet alles wat erachter in de wachtrij staat blokkeert of stilzwijgend verwijdert.

Elk systeem dat op berichten is gebaseerd, ontvangt uiteindelijk een bericht dat het niet kan afhandelen: een slecht gevormde payload, een schema dat is gewijzigd onder de consumer, of een downstream-aanroep die faalt ongeacht hoe vaak je het opnieuw probeert. Zonder een dead-letter queue blokkeert dat bericht de kop van de wachtrij voor altijd of wordt het stilzwijgend weggegooid, en beide uitkomsten zijn erger dan weten dat de fout is opgetreden.

Een DLQ verandert een onzichtbare fout in een zichtbare, inspecteerbare. Het geeft je een plek om het bericht te isoleren, erop te alerten en bewust te beslissen — niet per ongeluk — of je het moet repareren en opnieuw afspelen of definitief discarden.

dead letter queue routing failed messages away from the main queue

De specificaties verschillen per broker, maar het onderliggende patroon is overal hetzelfde: een teller voor leveringspogingen, een drempelwaarde en een bestemming voor berichten die deze overschrijden. Deze gids behandelt wat een DLQ precies doet, hoe je een poison-bericht kunt onderscheiden van een tijdelijke fout, wanneer je moet opnieuw proberen versus discarden, en hoe je veilig kunt herstarten nadat je de oorzaak hebt opgelost. Voor de bredere context van integratiepatronen waarin dit patroon past, zie App Architecture.

Wat is een Dead Letter Queue

Een dead-letter queue is een aparte, gewone wachtrij waarnaar een broker of consumer een bericht routeert nadat dat bericht te vaak is mislukt tijdens de verwerking. Het is geen speciaal construct — de dead-letter queue van RabbitMQ is een reguliere wachtrij die gebonden is aan een reguliere exchange, en een SQS DLQ is een reguliere standaard- of FIFO-wachtrij. Wat een wachtrij tot een “DLQ” maakt, is puur dat iets anders mislukte berichten daarheen stuurt.

flowchart LR P[Producer] --> Q[Main Queue] Q --> C[Consumer] C -- ack: success --> Done[Message deleted] C -- fail / nack / timeout --> Q Q -- retry budget exhausted --> DLQ[Dead Letter Queue] DLQ --> I[Inspect / alert] I -- fix root cause --> R[Replay to main queue] I -- unrecoverable --> D[Archive / discard]

Elke broker implementeert de omleiding anders:

  • Amazon SQS gebruikt een redrive-beleid met een maxReceiveCount. Zodra een bericht dat vaak is ontvangen zonder te worden verwijderd, verplaatst SQS het naar de geconfigureerde deadLetterTargetArn. AWS adviseert expliciet om de bewaartermijn van de DLQ langer in te stellen dan die van de bronwachtrij, omdat de oorspronkelijke enqueue-tijdstip — niet het verplaatsingstijdstip — nog steeds de vervaldatum bepaalt.
  • RabbitMQ dead-letter een bericht wanneer deze wordt afgewezen met requeue=false, de per-bericht TTL verloopt, de wachtrij een lengtelimiet bereikt, of een quorum-wachtrij de delivery-limit overschrijdt. Je configureert dit met de x-dead-letter-exchange (en optioneel x-dead-letter-routing-key) wachtrijargumenten, en RabbitMQ voegt x-death-headers toe die de reden, de oorspronkelijke wachtrij en het aantal keren vastleggen.
  • Apache Kafka heeft geen broker-native DLQ. Kafka volgt alleen offsets; het heeft geen concept van een “mislukt” bericht. Het dead-letter topic-patroon is iets dat je in de consumer bouwt, in een Kafka Streams-topologie, of in een Kafka Connect-connector — vaak gepaard met een retry-topic-tier voor de uiteindelijke DLT, zoals Spring Kafka’s @RetryableTopic en DeadLetterPublishingRecoverer dat doen.
  • Azure Service Bus dead-lettert automatisch zodra het leveringscount van een bericht MaxDeliveryCount overschrijdt (standaard 10), en ook voor een handvol systeemredenen zoals TTLExpiredException, HeaderSizeExceeded en MaxTransferHopCountExceeded, elk vastgelegd in de DeadLetterReason-eigenschap van het bericht.

Voor een breder overzicht van hoe brokers en streamingplatforms operationeel samenhangen in plaats van als betrouwbaarheids patroon, behandelen Apache Kafka Quickstart en RabbitMQ on AWS EKS vs SQS de infrastructuurkant van het draaien van deze brokers.

Poison Messages

Een poison-bericht is er één dat nooit zal slagen, ongeacht hoe vaak een consument het opnieuw probeert — een slecht gevormde JSON-payload, een schema-veld dat een producent heeft hernoemd, een overtreding van een business rule, of een bug die elke keer uitzet op een specifieke invoer. Dat verschilt van een tijdelijke fout, waarbij het bericht in orde is maar de omgeving het even niet is: een downstream-timeout, een database-connection-bliptje, een rate-limit-respons.

Het behandelen van beide soorten fouten op dezelfde manier is de meest voorkomende DLQ-fout. Als je bij de eerste fout dead-lettert, straf je tijdelijke fouten af die bij herwerking hadden kunnen slagen. Als je poison-berichten tientallen keren opnieuw probeert voordat je ophoudt, verspil je berekening, vertraging gerelateerde berichten achter hen (op geordende wachtrijen en partitions), en overlaad je je logs met dezelfde stacktrace.

Enkele detectiesignalen helpen bij het onderscheiden:

  • Uitzonderingstype. Deserialisatiefouten, validatiefouten en ClassCastException-achtige fouten zijn vrijwel altijd permanent. Spring Kafka’s DefaultErrorHandler behandelt bepaalde uitzonderingen expliciet als fataal en slaat herwerkingen over in plaats van eerst het herwerkingsbudget te laten opbranden.
  • Herhaaldelijk aantal zonder variatie. RabbitMQ’s x-death header-array laat je precies zien hoeveel keer een bericht is dead-lettered en waarom; een bericht met een groeiend aantal en een identieke x-first-death-reason op elke cyclus is poison, niet ongelukkig.
  • Consistente fout over replicas. Als elke consumenteninstantie faalt op hetzelfde bericht terwijl het succesvol is op alles eromheen, is het bericht zelf het probleem, niet de infrastructuur.

Om onderscheid te maken tussen herwerkbaar en niet-herwerkbaar fouten op codeniveau — dezelfde classificatie waar een DLQ-beleid op is gebaseerd — zie Go Error Handling Architecture: Boundaries and Patterns.

Retry vs Discard

De kernbeleidbeslissing achter elke DLQ is de retry-drempel: hoeveel leveringspogingen een bericht krijgt voordat het wordt geïsoleerd. Stel dit te laag in en je dead-lettert berichten die na een korte downstream-hiccup hadden kunnen slagen. Stel het te hoog in en een poison-bericht zit lang in de hoofdqueue, waardoor het werkcapaciteit verbruikt en — op geordende systemen — alles wat erachter in de wachtrij staat blokkeert.

De huidige aanbevelingen voor de grote brokers komen overeen op vergelijkbare getallen:

Broker Mechanisme Typische drempel
Amazon SQS maxReceiveCount in redrive-beleid 3–5 voor gemengde transiënte/permanente workloads
RabbitMQ (quorum wachtrijen) delivery-limit beleidargument 3–5, afgestemd per wachtrij
Azure Service Bus MaxDeliveryCount Standaard 10, vaak verminderd voor latentiekritieke wachtrijen
Kafka (via retry topics) Retry-count header + retry-topic tier 3–4 retry-topic hops voor de uiteindelijke DLT

Een praktische middenweg waarop veel teams landen is: begin conservatief (2–3 pogingen), observeer het feitelijke foutenmix in productie, en verhoog de drempel alleen voor wachtrijen waar je kunt aantonen dat de meeste fouten binnen een paar herwerkingen worden opgelost. Koppel het retry-aantal aan exponentiële backoff en jitter tussen pogingen zodat een downstream-uitval niet verandert in een retry-storm — dezelfde discipline die wordt behandeld in backoff- en circuitbreaker-ontwerp. Een circuit breaker aan de integratiegrens vult dit aan: het stopt met het versturen van verzoeken naar een ongezonde afhankelijkheid in plaats van elke individuele boodschap in de wachtrij de uitval te laten ontdekken en één voor één te dead-letteren.

Zodra een bericht in de DLQ staat, moet “discard” nog steeds een bewuste actie zijn, niet veronachtzaming. Stel een bewaartermijn in op de DLQ zelf — lang genoeg om te onderzoeken (AWS adviseert dat de DLQ-bewaartermijn die van de bronwachtrij overstijgt; een week is een veelvoorkomende vloer voor RabbitMQ DLQ’s) — en alerteer op DLQ-diepte en -leeftijd zodat fouten worden getriageerd in plaats van stilzwijgend te vervallen. Een bericht dat ouder wordt van de DLQ zonder het te onderzoeken, is een bericht dat je besloot te verliezen zonder te besluiten het te verliezen.

Idempotentie is hier net zo belangrijk als overal anders: duplicaten kunnen voorkomen; een bericht dat van een DLQ terug naar de hoofdqueue wordt gedraaid is, functioneel, een dubbele levering. Als je consument niet veilig twee keer op hetzelfde bericht kan worden uitgevoerd, kan het terugdraaien van een DLQ de exacte duplicaat-zij-effect-bug creëren die je probeerde te vermijden. Zie Idempotency in Distributed Systems That Actually Works voor de consumenten-patronen die redrive veilig maken.

Replay Strategies

Het correct uit een DLQ krijgen van een bericht is zijn eigen discipline, gescheiden van het erin krijgen.

  1. Los eerst de oorzaak op. Het implementeren van de consument-fix voordat je herstart, maakt het verschil tussen een schone herstel en het opnieuw vergiftigen van de wachtrij met dezelfde fout een tweede keer.
  2. Redrive bewust, niet automatisch. SQS ondersteunt een redrive-naar-bron-functie die berichten op verzoek terug naar hun oorspronkelijke wachtrij (of een andere bestemming) verplaatst; RabbitMQ en Kafka vereisen dat je de equivalent consument of tooling zelf bouwt. Behandel herstart in beide gevallen als een door operator gestarte actie met een record van wat is herstart en wanneer.
  3. Behoud ordening waar het belangrijk is. Voor Kafka moet de dead-letter-topic ten minste evenveel partitions hebben als de bron-topic en moet het de oorspronkelijke boodschapskey behouden, zodat herstarte berichten terug op de juiste partition landen en per-key ordening behouden.
  4. Beperk herstartpogingen. Een bericht dat opnieuw faalt na een fix-en-herstart-cyclus is niet tijdelijk; route het naar een permanent archief (een database-tabel, een object-storage-bucket) in plaats van het oneindig door de DLQ te laten lopen. RabbitMQ’s eigen documentatie waarschuwt dat een dead-lettered bericht slechts een beperkt aantal keren tussen wachtrijen kan worden gerouteerd (16) voordat verdere TTL-gebaseerde dead-lettering wordt uitgeschakeld.
  5. Laat een DLQ nooit zichzelf dead-letteren. Als je DLQ zijn eigen x-dead-letter-exchange (RabbitMQ) of eigen redrive-beleid (SQS) heeft dat terugwijst naar dezelfde keten, kan een herstartfout een oneindige lus creëren. Houd de eigen dead-letter-configuratie van de DLQ leeg, of wijs deze naar een strikt terminaal archief.
  6. Alerteer op volume, niet alleen op aanwezigheid. Een enkel bericht in een DLQ is een datapunt; een plotselinge piek is een incident. Koppel DLQ-diepte en boodschapsleeftijd aan dezelfde alerting-pipeline die je voor alles anders gebruikt — zie Modern Alerting Systems Design for Observability Teams voor routing- en noise-reductiepraktijken die direct van toepassing zijn op DLQ-alerts.

Als je workflow bestaat uit multi-stap, langlopende processen in plaats van enkele berichten, geldt hetzelfde dead-letter-denken op het workflowlaag — een saga’s compensatielogica heeft dezelfde “quarantaine, inspecteer, besluit” discipline nodig wanneer een stap permanent faalt in plaats van tijdelijk. En wanneer de gebeurtenissen zelf voortkomen uit een database-schrijfbewerking, bouwt het transactional outbox pattern dead-letterafhandeling al in de relay-werknemer, dus verschijnt het patroon een laag eerder dan de broker.

Waar DLQ’s passen in het grotere geheel

Een dead-letter queue maakt fouten niet onzichtbaar — het maakt ze overlevend en naspeurbaar in plaats van stilzwijgend. Het werkt het beste samen met herwerkingen met backoff voor het transiënte geval, idempotente consumenten zodat redrive veilig is, en een circuit breaker zodat een worstelende afhankelijkheid de hoofdqueue (en uiteindelijk de DLQ) niet overlaadt met dezelfde fout duizenden keren. Behandel de DLQ-drempel, bewaartermijn en alerting als eerste-klas configuratiebeslissingen, geen standaardwaarden die je met rust laat, en dead letters worden een diagnostisch hulpmiddel in plaats van een plek waar data stilzwijgend verdwijnt.

Abonneren

Ontvang nieuwe berichten over systemen, infrastructuur en AI-engineering.