Dead Letter Queues: Umgang mit Gift-Nachrichten in verteilten Systemen
Verhindern Sie, dass vergiftete Nachrichten Warteschlangen blockieren
Eine Dead-Letter-Queue (DLQ) ist das Sicherheitsnetz, das Nachrichten einfängt, die deine Konsumenten nicht verarbeiten können, sodass ein defektes Payload nicht alles Weitere in der Warteschlange blockiert oder stillschweigend verwirft.
Jedes nachrichtengetriebene System erhält irgendwann eine Nachricht, die es nicht verarbeiten kann: ein fehlerhaftes Payload, ein Schema, das sich unterhalb des Konsumenten geändert hat, oder ein Aufruf an eine abhängige Komponente, der fehlschlägt, egal wie oft du es wiederholst. Ohne eine Dead-Letter-Queue blockiert diese Nachricht entweder dauerhaft den Kopf der Warteschlange oder wird stillschweigend verworfen, und beide Ergebnisse sind schlechter als das Wissen um den Fehler.
Eine DLQ verwandelt einen unsichtbaren Fehler in einen sichtbaren, überprüfbaren. Sie bietet dir einen Ort, um die Nachricht zu isolieren, eine Warnung auszulösen und bewusst zu entscheiden – nicht durch Zufall –, ob du sie beheben und erneut versuchen möchtest oder endgültig verwerfen willst.

Die Mechaniken unterscheiden sich je nach Broker, aber das zugrunde liegende Muster ist überall gleich: eine Zählung der Zustellversuche, ein Schwellenwert und ein Zielort für Nachrichten, die diesen überschreiten. Dieser Leitfaden behandelt, was eine DLQ eigentlich tut, wie du eine „Poison Message“ von einem transienten Fehler unterscheidest, wann du erneut versuchen solltest versus verwerfen, und wie du sicher wiederholen kannst, sobald du die Ursache behoben hast. Für den breiteren Kontext der Integrationsmuster, in dem dieses Muster eingebettet ist, siehe App Architecture.
Was ist eine Dead-Letter-Queue?
Eine Dead-Letter-Queue ist eine separate, normale Warteschlange, zu der ein Broker oder Konsument eine Nachricht verschiebt, nachdem diese zu oft bei der Verarbeitung fehlgeschlagen ist. Es handelt sich nicht um ein spezielles Konstrukt – die Dead-Letter-Queue von RabbitMQ ist eine reguläre Warteschlange, die an einen regulären Austausch gebunden ist, und eine SQS-DLQ ist eine reguläre Standard- oder FIFO-Warteschlange. Was eine Warteschlange zu einer „DLQ“ macht, ist rein dadurch bedingt, dass etwas anderes fehlgeschlagene Nachrichten dorthin weiterleitet.
Jeder Broker implementiert die Weiterleitung anders:
- Amazon SQS verwendet eine Redrive-Richtlinie mit einem
maxReceiveCount. Sobald eine Nachricht so oft empfangen wurde, ohne gelöscht zu werden, verschiebt SQS sie zum konfiguriertendeadLetterTargetArn. AWS empfiehlt explizit, den Nachrichtenbehaltungszeitraum der DLQ länger als den der Quellwarteschlange zu halten, da der ursprüngliche Enqueue-Zeitstempel – nicht der Verschiebezeitpunkt – weiterhin die Ablaufzeit steuert. - RabbitMQ dead-lettered eine Nachricht, wenn sie mit
requeue=falseabgelehnt wird, ihre pro-Nachrichten-TTL abläuft, die Warteschlange eine Längenbegrenzung erreicht oder eine Quorum-Warteschlange ihrendelivery-limitüberschreitet. Dies konfigurierst du mit den Warteschlangenargumentenx-dead-letter-exchange(und optionalx-dead-letter-routing-key), und RabbitMQ fügtx-death-Header hinzu, die den Grund, die Ursprungswarteschlange und wie oft dies geschah, aufzeichnen. - Apache Kafka verfügt über keine broker-native DLQ. Kafka verfolgt nur Offsets; es hat kein Konzept einer „fehlgeschlagenen“ Nachricht. Das Dead-Letter-Topic-Muster ist etwas, das du im Konsumenten, in einer Kafka-Streams-Topologie oder in einem Kafka-Connect-Connector selbst aufbaust – häufig gepaart mit einer Retry-Topic-Stufe vor dem terminalen DLT, wie es Spring Kafkas
@RetryableTopicundDeadLetterPublishingRecoverertun. - Azure Service Bus dead-lettered automatisch, sobald die Delivery-Count einer Nachricht
MaxDeliveryCount(Standard 10) überschreitet, sowie auch für einige Systemgründe wieTTLExpiredException,HeaderSizeExceededundMaxTransferHopCountExceeded, die jeweils in der EigenschaftDeadLetterReasonder Nachricht aufgezeichnet werden.
Für einen breiteren Überblick darüber, wie Broker und Streaming-Plattformen operativ zusammenpassen – statt als Zuverlässigkeitsmuster –, decken Apache Kafka Quickstart und RabbitMQ on AWS EKS vs SQS die Infrastrukturseite des Betriebs dieser Broker ab.
Poison Messages
Eine Poison Message ist eine Nachricht, die niemals erfolgreich sein wird, egal wie oft ein Konsument sie wiederholt – ein fehlerhaftes JSON-Payload, ein Schemafeld, das ein Produzent umbenannt hat, ein Verstoß gegen eine Geschäftsregel oder ein Fehler, der bei einer bestimmten Eingabe jedes Mal eine Ausnahme auslöst. Das unterscheidet sich von einem transienten Fehler, bei dem die Nachricht in Ordnung ist, die Umgebung jedoch kurzzeitig nicht: ein Timeout einer abhängigen Komponente, ein kurzer Ausfall der Datenbankverbindung oder eine Antwort wegen Rate-Limits.
Beide Fehlertypen gleich zu behandeln, ist der häufigste Fehler bei DLQs. Wenn du beim ersten Fehler dead-lettered, bestrafst du transiente Fehler, die bei einer Wiederholung erfolgreich gewesen wären. Wenn du Poison Messages Dutzende Male wiederholst, bevor du aufgibst, verschwendest du Rechenleistung, verzögerst andere Nachrichten hinter ihnen (bei geordneten Warteschlangen und Partitionen) und flutest deine Logs mit dem gleichen Stacktrace.
Einige Erkennungssignale helfen dabei, die beiden zu trennen:
- Ausnahmetyp. Deserialisierungsfehler, Validierungsfehler und Fehler vom Typ
ClassCastExceptionsind fast immer permanent. Spring KafkasDefaultErrorHandlerbehandelt bestimmte Ausnahmen explizit als fatal und überspringt die Wiederholungen für sie, anstatt zuerst das Wiederholungsbudget aufzubrauchen. - Wiederholungszahl ohne Varianz. Der
x-death-Header-Array von RabbitMQ ermöglicht es dir zu sehen, wie oft eine Nachricht dead-lettered wurde und warum; eine Nachricht mit einer wachsenden Zählung und einem identischenx-first-death-reasonbei jedem Zyklus ist eine Poison Message, kein Pech. - Konsistenter Fehler über Repliken hinweg. Wenn jede Konsumenteninstanz bei derselben Nachricht fehlschlägt, während sie bei allem anderen erfolgreich ist, ist die Nachricht selbst das Problem, nicht die Infrastruktur.
Um zwischen wiederholbaren und nicht-wiederholbaren Fehlern auf Codeebene zu unterscheiden – derselben Klassifizierung, auf der eine DLQ-Richtlinie basiert –, siehe Go Error Handling Architecture: Boundaries and Patterns.
Retry vs. Discard
Die Kernrichtlinienentscheidung hinter jeder DLQ ist der Wiederholungsschwellenwert: Wie viele Zustellversuche erhält eine Nachricht, bevor sie in Quarantäne versetzt wird. Legst du diesen zu niedrig fest, dead-lettered du Nachrichten, die nach einem kurzen Ausfall einer abhängigen Komponente erfolgreich gewesen wären. Legst du ihn zu hoch fest, sitzt eine Poison Message lange Zeit in der Hauptwarteschlange, verbraucht Worker-Kapazität und – in geordneten Systemen – blockiert alles, was dahinter in der Warteschlange steht.
Die aktuelle Empfehlung über die großen Broker hinweg konvergiert zu ähnlichen Zahlen:
| Broker | Mechanismus | Typischer Schwellenwert |
|---|---|---|
| Amazon SQS | maxReceiveCount in Redrive-Richtlinie |
3–5 für gemischte transiente/permanente Workloads |
| RabbitMQ (Quorum-Warteschlangen) | delivery-limit-Richtlinienargument |
3–5, je nach Warteschlange angepasst |
| Azure Service Bus | MaxDeliveryCount |
Standard 10, oft reduziert für latenzempfindliche Warteschlangen |
| Kafka (über Retry-Themen) | Retry-Count-Header + Retry-Topic-Stufe | 3–4 Retry-Topic-Hops vor dem terminalen DLT |
Ein praktischer Mittelweg, auf den viele Teams stoßen, ist: Konservativ beginnen (2–3 Versuche), die tatsächliche Fehlermischung in der Produktion beobachten und den Schwellenwert nur für Warteschlangen erhöhen, bei denen du nachweisen kannst, dass die meisten Fehler innerhalb weniger Wiederholungen aufgelöst werden. Kombiniere die Wiederholungszahl mit exponentieller Backoff und Jitter zwischen den Versuchen, damit ein Ausfall einer abhängigen Komponente nicht zu einem Retry-Sturm wird – dieselbe Disziplin, die in Backoff- und Circuit-Breaker-Designs behandelt wird. Ein Circuit Breaker am Integrationsrand ergänzt dies: Er stoppt das Senden von Anforderungen an eine instabile Abhängigkeit, anstatt jede Nachricht in der Warteschlange einzeln den Ausfall entdecken und nacheinander dead-lettered zu lassen.
Sobald eine Nachricht in der DLQ ist, sollte „verwerfen“ immer eine bewusste Aktion sein, nicht Vernachlässigung. Lege einen Behaltungszeitraum für die DLQ selbst fest – lang genug zur Untersuchung (AWS empfiehlt, dass der DLQ-Beibehaltungszeitraum den der Quellwarteschlange übersteigt; eine Woche ist ein gemeinsamer Mindestwert für RabbitMQ-DLQs) – und alarmiere auf DLQ-Tiefe und -Alter, damit Fehler triagiert werden, anstatt stillschweigend zu verfallen. Eine Nachricht, die ununtersucht aus der DLQ abläuft, ist eine Nachricht, deren Verlust du beschlossen hast, ohne es bewusst zu beschließen.
Idempotenz ist hier genauso wichtig wie überall sonst – Duplikate können auftreten: Eine Nachricht, die von einer DLQ zurück in die Hauptwarteschlange redriven wird, ist funktionell eine doppelte Zustellung. Wenn dein Konsument nicht sicher ist, zweimal mit derselben Nachricht ausgeführt zu werden, kann das Redriven aus einer DLQ genau den Duplikat-Nebenwirkungsfehler erzeugen, den du vermeiden wolltest. Siehe Idempotency in Distributed Systems That Actually Works, um die Konsumentenseitenmuster zu erfahren, die das Redriven sicher machen.
Replay-Strategien
Das korrekte Herausfinden einer Nachricht aus der DLQ ist eine eigene Disziplin, getrennt vom Hineinfinden.
- Behebe zuerst die Ursache. Das Bereitstellen der Konsumentenbehebung vor dem Wiederholen ist der Unterschied zwischen einer sauberen Wiederherstellung und dem erneuten Vergiften der Warteschlange mit dem gleichen Fehler zum zweiten Mal.
- Redrive bewusst, nicht automatisch. SQS unterstützt eine Redrive-to-Source-Funktion, die Nachrichten auf Nachfrage zurück in ihre ursprüngliche Warteschlange (oder ein anderes Ziel) verschiebt; RabbitMQ und Kafka erfordern, dass du den entsprechenden Konsumenten oder Tooling selbst erstellst. Auf jeden Fall behandle das Replay als eine vom Operator ausgelöste Aktion mit einer Aufzeichnung dessen, was wiederholt wurde und wann.
- Ordnung dort bewahren, wo sie wichtig ist. Für Kafka sollte das Dead-Letter-Topic mindestens so viele Partitionen wie das Quelltopic haben und den ursprünglichen Nachrichten-Schlüssel beibehalten, damit wiederholte Nachrichten auf die richtige Partition zurückkehren und die pro-Schlüssel-Ordnung wahren.
- Wiederholungsversuche begrenzen. Eine Nachricht, die nach einem Fix-and-Replay-Zyklus erneut fehlschlägt, ist nicht transient – leite sie in ein permanentes Archiv (eine Datenbanktabelle, einen Object-Storage-Bucket) statt sie endlos durch die DLQ zu schleifen. Die eigenen Docs von RabbitMQ warnen, dass eine dead-lettered Nachricht nur eine begrenzte Anzahl von Malen (16) zwischen Warteschlangen weitergeleitet werden kann, bevor die weitere TTL-basierte Dead-Lettering-Funktion deaktiviert wird.
- Lasse eine DLQ niemals in sich selbst dead-letteren. Wenn deine DLQ ihre eigene
x-dead-letter-exchange(RabbitMQ) oder ihre eigene Redrive-Richtlinie (SQS) hat, die zurück zur gleichen Kette zeigt, kann ein Replay-Fehler eine Endlosschleife erzeugen. Halte die Dead-Letter-Konfiguration der DLQ selbst leer, oder weise sie auf ein strikt finales Archiv. - Alarmiere bei Volumen, nicht nur bei Vorhandensein. Eine einzelne Nachricht in einer DLQ ist ein Datenpunkt; ein plötzlicher Anstieg ist ein Vorfall. Verbinde DLQ-Tiefe und Nachrichtenalter mit derselben Alarmierungs-Pipeline, die du für alles andere verwendest – sieh dir Modern Alerting Systems Design for Observability Teams an, um Routing- und Lärmreduzierungspraktiken zu erhalten, die direkt auf DLQ-Alarme anwendbar sind.
Wenn dein Workflow mehrstufige, langlaufende Prozesse statt einzelner Nachrichten involviert, gilt dasselbe Dead-Letter-Denken auf der Worklayer-Ebene – die Kompensationslogik einer Saga benötigt dieselbe Disziplin „Isolieren, Untersuchen, Entscheiden“, wenn ein Schritt permanent statt transient fehlschlägt. Und wenn die Ereignisse selbst von einem Datenbank-Schreiben stammen, baut das Transaction Outbox Pattern die Dead-Letter-Verarbeitung bereits in den Relay-Worker ein, sodass das Muster eine Ebene früher als beim Broker auftaucht.
Wo DLQs im größeren Bild passen
Eine Dead-Letter-Queue lässt Fehler nicht verschwinden – sie macht sie überlebens- und überprüfbar statt stillschweigend. Sie funktioniert am besten in Begleitung von Wiederholungen mit Backoff für den transienten Fall, idempotenten Konsumenten, damit das Redriven sicher ist, und eines Circuit Breakers, damit eine schwächelnde Abhängigkeit die Hauptwarteschlange (und schließlich die DLQ) nicht mit dem gleichen Fehler Tausende Male überflutet. Behandle den DLQ-Schwellenwert, den Behaltungszeitraum und die Alarmierung als erstklassige Konfigurationsentscheidungen, keine Standardwerte, die du unangetastet lässt, und Dead Letters werden zu einem Diagnosewerkzeug statt zu einem Ort, an dem Daten leise verschwinden.