Döda brevlådor: Att hantera giftiga meddelanden i distribuerade system
Stoppa giftmeddelanden från att blockera köer
En kö för döda brev (dead-letter queue) är säkerhetsnätet som fångar meddelanden som dina konsumenter inte kan bearbeta, så att ett trasigt innehåll inte blockerar eller tyst lämnar kvar allt bakom sig i kön.
Varje medelandriven system får till slut ett meddelande det inte kan hantera: ett felaktigt innehåll, ett schema som ändrats under konsumentens fötter, eller ett anrop till en beroende tjänst som misslyckas oavsett hur många gånger du försöker igen. Utan en kö för döda brev antingen blockerar det meddelandet i huvudkön för alltid eller så slängs det tyst bort, och båda utfallen är sämre än att veta om felet.
En DLQ (Dead Letter Queue) gör en osynlig felhantering till en synlig, granskbar sådan. Den ger dig en plats att karantäna meddelandet, varna för det, och bestämma – medvetet, inte av misstag – om du ska åtgärda och försöka igen eller kasta det för gott.

Mechaniken skiljer sig mellan olika brokrar, men underliggande mönstret är detsamma överallt: en räknare för leveransförsök, en tröskelvärde, och en destination för meddelanden som passerar den här gränsen. Den här guiden täcker vad en DLQ faktiskt gör, hur du skiljer ett giftigt meddelande från ett tillfälligt fel, när du ska försöka igen jämfört med att kasta bort, och hur du återuppar säker efter att ha åtgärrotorsaken. För den bredare kontexten av integrationsmönster som detta mönster ingår i, se App Architecture.
Vad är en Dead Letter Queue?
En dead-letter queue är en separat, vanlig kö som en brokr eller konsument flyttar ett meddelande till efter att meddelandet har misslyckats med bearbetningen för många gånger. Det är inte en speciell konstruktion — RabbitMQ:s dead-letter queue är en vanlig kö bunden till en vanlig exchange, och en SQS DLQ är en vanlig standard- eller FIFO-kö. Det som gör en kö till en “DLQ” är rent att något annat pekar misslyckade meddelanden åt den.
Varje brokr implementerar omdirigeringen annorlunda:
- Amazon SQS använder en redrive-policy med ett
maxReceiveCount. När ett meddelande har mottagits det antalet gånger utan att ha raderats, flyttar SQS det till den konfigureradedeadLetterTargetArn. AWS rekommenderar explicit att behålla DLQ:s meddelandeförvaringstid längre än källkön, eftersom original-tidsstämpeln för köning — inte flyttiden — fortfarande styr utgång. - RabbitMQ dödar ett meddelande när det avslås med
requeue=false, dess per-meddelande-TTL går ut, kön når en längdgräns, eller en quorum-kö överskrider sittdelivery-limit. Du konfigurerar detta medx-dead-letter-exchange(och valfrittx-dead-letter-routing-key) köargument, och RabbitMQ fästerx-death-huvuden som registrerar anledningen, ursprungskön och hur många gånger det hände. - Apache Kafka har ingen brokr-inbyggd DLQ. Kafka spårar endast offset; den har inget begrepp om ett “misslyckat” meddelande. Mönstret för dead-letter-ämnet är något du bygger i konsumenten, i en Kafka Streams-topologi, eller i en Kafka Connect-connector — vanligtvis parat med en retry-topic-tier innan den terminala DLT, som Spring Kafka:s
@RetryableTopicochDeadLetterPublishingRecoverergör. - Azure Service Bus dödar automatiskt ett meddelande när dess leveransräknare överskrider
MaxDeliveryCount(standard 10), och även för ett fåtal systemskäl såsomTTLExpiredException,HeaderSizeExceeded, ochMaxTransferHopCountExceeded, varav varje registreras i meddelandetsDeadLetterReason-egenskap.
För en bredare bild av hur brokrar och streamningsplattformar hänger ihop operativt snarare än som ett tillförlitlighetsmönster, Apache Kafka Quickstart och RabbitMQ på AWS EKS vs SQS täcker infrastrukturen för att köra dessa brokrar.
Giftiga meddelanden
Ett giftigt meddelande är ett sådant som aldrig kommer att lyckas oavsett hur många gånger en konsument försöker igen — ett felaktigt JSON-innehåll, ett schemafält som en producent döpt om, ett affärsregelsbrott, eller en bugg som kastar ett undantag vid en specifik indata varje gång. Det är annorlunda än ett tillfälligt fel, där meddelandet är fint men miljön kortvarigt inte är det: en timeout till en beroende tjänst, en databasanslutningsavbrott, ett svarsmeddelande om hastighetsbegränsning.
Att behandla båda misslyckandetyperna på samma sätt är det vanligaste DLQ-misstaget. Om du dödar vid första misslyckandet straffar du tillfälliga fel som skulle ha lyckats vid en omförsök. Om du försöker igen med giftiga meddelanden tiotals gånger innan du ger upp, slösar du beräkningskraft, fördröjer orelaterade meddelanden bakom dem (på ordnade köer och partitioner), och översvämmar dina loggar med samma stacktrace.
Flera detektionssignaler hjälper till att åtskilja de två:
- Undantagstyp. Deserialiseringsfel, valideringsfel och
ClassCastException-liknande misslyckanden är nästan alltid permanenta. Spring Kafka:sDefaultErrorHandlerbehandlar explicit vissa undantag som dödliga och hoppar över omförsök för dem istället för att uttöma omförsöksbudgeten först. - Upprepningsräkning utan variation. RabbitMQ:s
x-death-huvudarray låter dig se exakt hur många gånger ett meddelande har dödlats och varför; ett meddelande med en växande räknare och en identiskx-first-death-reasonvid varje cykel är giftigt, inte otur. - Konsistent misslyckande över repliker. Om varje konsumentinstans misslyckas på samma meddelande medan den lyckas med allt runt omkring det, är meddelandet i sig problemet, inte infrastrukturen.
För att åtskilja omförsökbara från icke-omförsökbara fel på kodnivå — samma klassificering en DLQ-policy beror på — se Go Error Handling Architecture: Boundaries and Patterns.
Försök igen vs Kasta bort
Den grundläggande policybeslutet bakom varje DLQ är omförsökströskeln: hur många leveransförsök ett meddelande får innan det karantänns. Få detta för lågt och du dödar meddelanden som skulle ha lyckats efter ett kortvarigt bakhäng hos en beroende tjänst. Få detta för högt och ett giftigt meddelande sitter i huvudkön länge, vilket förbrukar arbetskapacitet och — på ordnade system — blockerar allt som köats bakom det.
Aktuell vägledning över de stora brokrarna konvergerar mot liknande nummer:
| Brokr | Mechanism | Typisk tröskel |
|---|---|---|
| Amazon SQS | maxReceiveCount i redrive-policy |
3–5 för blandade transienta/permanenta arbetsbelastningar |
| RabbitMQ (quorum-köer) | delivery-limit policyargument |
3–5, inställt per kö |
| Azure Service Bus | MaxDeliveryCount |
Standard 10, minskas ofta för latenskänsliga köer |
| Kafka (via retry-ämnen) | Retry-count-huvud + retry-topic-tier | 3–4 retry-topic-hoppar innan den terminala DLT |
En praktisk mellanväg många team landar på är: börja konservativt (2–3 försök), övervak den faktiska blandningen av fel i produktion, och höj tröskeln endast för köer där du kan visa att de flesta fel löses inom några omförsök. Para ihop omförsöksantalet med exponentiell backoff och jitter mellan försök så att ett bortfall hos en beroende tjänst inte blir en storm av omförsök — samma disciplin som täcks i designen för backoff och brytarskydd. En brytarskydd vid integrationsgränsen kompletterar detta: det slutar skicka begäranden till en ohälsosam beroende istället för att låta varje meddelande i kön ensamt upptäcka bortfallet och dödas ett i taget.
När ett meddelande väl är i DLQ, ska “kasta bort” fortfarande vara en medveten handling, inte försumelse. Ställ in en förvaringstid på DLQ:n i sig — tillräckligt lång för att undersöka (AWS rekommenderar att DLQ-förvaringen överstiger källkön; en vecka är en vanlig golvgräns för RabbitMQ DLQs) — och varna för DLK-djup och ålder så att fel får prioriteras istället för att tyst gå ut. Ett meddelande som åldras ut ur DLQ:n oidentifierat är ett meddelande du bestämde dig att förlora utan att bestämma dig för att förlora det.
Idempotens är lika viktig här som den är någon annanstans — dupliceringar kan ske: ett meddelande som körs om från en DLQ tillbaka till huvudkön är, funktionellt sett, en dubblettleverans. Om din konsument inte är säker att köra två gånger på samma meddelande, kan omkörning från en DLQ skapa exakt den duplikat-bisidsfel-bugg du försökte undvika. Se Idempotency in Distributed Systems That Actually Works för konsument-sidans mönster som gör omkörning säker.
Återuppspelningsstrategier
Att få ut ett meddelande ur DLQ:n korrekt är sin egen disciplin, separat från att få det dit.
- Åtgärda rotorsaken först. Att distribuera konsumentfixen innan återuppspelning är skillnaden mellan en ren återhämtning och att återgifta kön med samma fel en andra gång.
- Kör om medvetet, inte automatiskt. SQS stöder en “redrive-to-source”-funktion som flyttar meddelanden tillbaka till deras ursprungliga kö (eller en annan destination) på begäran; RabbitMQ och Kafka kräver att du bygger motsvarande konsument eller verktyg själv. Oavsett hur, behanda återuppspelning som en operatör-utlöst åtgärd med en anteckning om vad som spelades upp och när.
- Behåll ordning där det spelar roll. För Kafka, bör dead-letter-ämnet ha minst lika många partitioner som källämnet och bör behålla den ursprungliga meddelandenöklarna, så att återuppspelade meddelanden landar tillbaka på rätt partition och behåller per-nyckel-ordning.
- Begränsa återuppspelningsförsök. Ett meddelande som misslyckas igen efter en åtgärda-och-återuppspelningscykel är inte tillfälligt — skicka det till en permanent arkiv (en databastabell, en objekt-lagringsbucket) istället för att loopa det genom DLQ:n oändligt. RabbitMQ:s egna dokument varnar för att ett dödlagt meddelande kan omdirigeras mellan köer bara ett begränsat antal gånger (16) innan ytterligare TTL-baserad dödläggning inaktiveras.
- Låt aldrig en DLQ döda in i sig själv. Om din DLQ har sin egen
x-dead-letter-exchange(RabbitMQ) eller sin egen redrive-policy (SQS) pekande tillbaka mot samma kedja, kan ett återuppspelningsfel skapa en oändlig loop. Håll DLQ:s egna dödläggningkonfiguration tom, eller peka den mot en strikt terminal arkiv. - Varna för volym, inte bara närvaro. Ett enskilt meddelande i en DLQ är en datapunkt; en plötslig topp är en incident. Koppla DLK-djup och meddelandeålder till samma varningspipeline du använder för allt annat — se Modern Alerting Systems Design for Observability Teams för ruttnings- och brusreduceringspraxis som gäller direkt för DLQ-varningar.
Om ditt arbetsflöde involverar multifasiga, långvariga processer snarare än enskilda meddelanden, gäller samma dödläggningstänkande på arbetsflödesnivå — en sagas kompenseringslogik behöver samma “karantän, undersök, bestäm”-disciplin när ett steg misslyckas permanent istället för tillfälligt. Och när händelserna själva härstammar från en databasskrivning, bygger transactional outbox-mönstret redan dödläggshantering i relay-arbetaren, så att mönstret dyker upp ett lager tidigare än brokraren.
Var DLQ:er passar in i den större bilden
En dead-letter queue gör inte fel försvinna — den gör dem överlevbara och granskbara istället för tysta. Den fungerar bäst tillsammans med omförsök med backoff för det tillfälliga fallet, idempotenta konsumenter så att omkörning är säker, och ett brytarskydd så att en kämpande beroende inte översvämmar huvudkön (och, till slut, DLQ:n) med samma fel tusentals gånger om. Behandla DLQ-tröskeln, förvaring och varningar som förstklassiga konfigurationsbeslut, inte standardvärden du läter orörda, och döda brev blir ett diagnostiskt verktyg istället för en plats där data tyst försvinner.