Dead Letter Queues: gestire i messaggi avvelenati nei sistemi distribuiti
Blocca i messaggi tossici che impediscono l’elaborazione delle code
Una coda di messaggi non elaborati (Dead-Letter Queue) è la rete di sicurezza che intercetta i messaggi che i consumer non riescono a elaborare, così che un singolo payload corrotto non blocchi o scarti silenziosamente tutti i messaggi successivi nella coda.
Ogni sistema guidato dai messaggi finisce per ricevere un messaggio che non può gestire: un payload malformato, uno schema modificato sottoposto al consumer, o una chiamata a un servizio downstream che fallisce indipendentemente da quante volte lo si riprova. Senza una dead-letter queue, quel messaggio bloccherà la testa della coda per sempre o verrà scartato silenziosamente, e entrambi gli esiti sono peggiori del sapere dell’errore.
Una DLQ trasforma un errore invisibile in uno visibile e ispezionabile. Fornisce un luogo dove quarantenare il messaggio, allertare su di esso e decidere — deliberatamente, non per caso — se correggere e riprovare a inviarlo o scartarlo definitivamente.

I meccanismi variano tra i diversi broker, ma il pattern sottostante è lo stesso ovunque: un contatore di tentativi di consegna, una soglia e una destinazione per i messaggi che la superano. Questa guida copre cosa fa effettivamente una DLQ, come distinguere un messaggio velenoso da un errore transitorio, quando riprovare versus scartare, e come riprovare a inviare in sicurezza una volta corretto il problema alla radice. Per il contesto più ampio dei pattern di integrazione in cui questo pattern si inserisce, vedere Architettura dell’App.
Cos’è una Dead Letter Queue
Una dead-letter queue è una coda ordinaria separata a cui un broker o un consumer instrada un messaggio dopo che questo ha fallito l’elaborazione per troppe volte. Non è una costruzione speciale: la dead-letter queue di RabbitMQ è una coda regolare associata a uno scambio regolare, e una DLQ di SQS è una coda standard o FIFO regolare. Ciò che rende una coda una “DLQ” è puramente il fatto che qualcos’altro le indirizza i messaggi falliti.
Ogni broker implementa il reindirizzamento in modo diverso:
- Amazon SQS utilizza una politica di redrive con un
maxReceiveCount. Una volta che un messaggio è stato ricevuto quel numero di volte senza essere eliminato, SQS lo sposta nell’deadLetterTargetArnconfigurato. AWS raccomanda esplicitamente di mantenere il periodo di ritenzione dei messaggi della DLQ più lungo di quello della coda sorgente, perché l’timestamp originale di accodamento — non il momento dello spostamento — governa ancora la scadenza. - RabbitMQ dead-letterizza un messaggio quando viene rifiutato con
requeue=false, scade la sua TTL per messaggio, la coda raggiunge un limite di lunghezza o una coda quorum supera il suodelivery-limit. Questo si configura con gli argomenti della codax-dead-letter-exchange(e opzionalmentex-dead-letter-routing-key), e RabbitMQ allega intestazionix-deathche registrano il motivo, la coda di origine e quante volte è successo. - Apache Kafka non ha una DLQ nativa del broker. Kafka traccia solo gli offset; non ha il concetto di messaggio “fallito”. Il pattern dell’argomento dead-letter è qualcosa che costruisci nel consumer, in una topologia Kafka Streams o in un connettore Kafka Connect — comunemente accoppiato a un tier retry-topic prima del DLT terminale, come fanno
@RetryableTopiceDeadLetterPublishingRecovererdi Spring Kafka. - Azure Service Bus dead-letterizza automaticamente una volta che il conteggio di consegna di un messaggio supera
MaxDeliveryCount(default 10), e anche per una manciata di motivi di sistema comeTTLExpiredException,HeaderSizeExceededeMaxTransferHopCountExceeded, ciascuno registrato nella proprietàDeadLetterReasondel messaggio.
Per una visione più ampia di come i broker e le piattaforme di streaming si integrano operativamente piuttosto che come pattern di affidabilità, Avvio rapido Apache Kafka e RabbitMQ su AWS EKS vs SQS coprono il lato infrastrutturale dell’esecuzione di questi broker.
Messaggi Velenosi
Un messaggio velenoso è uno che non avrà mai successo indipendentemente da quante volte un consumer lo riprova: un payload JSON malformato, un campo dello schema rinominato dal producer, una violazione della regola di business o un bug che genera un’eccezione su un input specifico ogni singola volta. Questo è diverso da un errore transitorio, dove il messaggio è a posto ma l’ambiente brevemente non lo è: un timeout downstream, un intoppo nella connessione al database, una risposta di rate limit.
Trattare entrambi i tipi di errore nello stesso modo è l’errore più comune nelle DLQ. Se si effettua il dead-lettering al primo fallimento, si puniscono gli errori transienti che avrebbero avuto successo al riprovarci. Se si riprova un messaggio velenoso decine di volte prima di arrendersi, si spreca capacità di calcolo, si ritardano i messaggi non correlati dietro di esso (su code ordinate e partizioni), e si inondano i log con la stessa traccia dello stack.
Alcuni segnali di rilevamento aiutano a separare i due casi:
- Tipo di eccezione. Gli errori di deserializzazione, gli errori di validazione e le fallimenti di tipo
ClassCastExceptionsono quasi sempre permanenti. IlDefaultErrorHandlerdi Spring Kafka tratta esplicitamente alcune eccezioni come fatali e salta i riprovati per esse invece di esaurire prima il budget di riprova. - Contatore ripetuto senza varianza. L’array di intestazioni
x-deathdi RabbitMQ ti permette di vedere esattamente quante volte un messaggio è stato dead-letterizzato e perché; un messaggio con un conteggio crescente e unx-first-death-reasonidentico in ogni ciclo è velenoso, non sfortunato. - Fallimento consistente attraverso i replica. Se ogni istanza del consumer fallisce sullo stesso messaggio mentre ha successo su tutto il resto, il messaggio stesso è il problema, non l’infrastruttura.
Per distinguere a livello di codice i fallimenti riprovabili da quelli non riprovabili — la stessa classificazione su cui si basa la politica DLQ — vedere Architettura Gestione Errori Go: Confini e Pattern.
Riprova vs Scarta
La decisione di politica principale dietro ogni DLQ è la soglia di riprova: quanti tentativi di consegna riceve un messaggio prima di essere quarantenato. Impostarla troppo bassa e si dead-letterizza messaggi che avrebbero avuto successo dopo un breve intoppo downstream. Impostarla troppo alta e un messaggio velenoso rimane nella coda principale per molto tempo, consumando la capacità dei worker e — sui sistemi ordinati — bloccando tutto ciò che è accodato dietro di esso.
Le linee guida attuali sui principali broker convergono su numeri simili:
| Broker | Meccanismo | Soglia tipica |
|---|---|---|
| Amazon SQS | maxReceiveCount nella politica di redrive |
3–5 per carichi di lavoro misti transienti/permanenti |
| RabbitMQ (code quorum) | Argomento politica delivery-limit |
3–5, ottimizzato per coda |
| Azure Service Bus | MaxDeliveryCount |
Default 10, spesso ridotto per code sensibili alla latenza |
| Kafka (tramite retry topics) | Retry-count header + tier retry-topic | 3–4 salti retry-topic prima del DLT terminale |
Un punto medio pratico su cui molte squadre si fermano è: iniziare conservativi (2–3 tentativi), osservare la mescolanza effettiva dei fallimenti in produzione e aumentare la soglia solo per le code dove si può dimostrare che la maggior parte dei fallimenti si risolve entro pochi riprovati. Accoppiare il conteggio delle riprove con esponenziale backoff e jitter tra i tentativi così un’interruzione downstream non si trasformi in una tempesta di riprova — la stessa disciplina coperta nel design del backoff e del circuit breaker. Un circuit breaker al confine di integrazione completa questo approccio: impedisce di inviare richieste a una dipendenza non sana invece di far sì che ogni messaggio nella coda scopri individualmente l’interruzione e si dead-letterizzi uno alla volta.
Una volta che un messaggio è nella DLQ, lo “scarto” dovrebbe comunque essere un’azione deliberata, non negligenza. Impostare un periodo di ritenzione sulla DLQ stessa — abbastanza lungo per indagare (AWS raccomanda che la ritenzione della DLQ superi quella della coda sorgente; una settimana è un pavimento comune per le DLQ di RabbitMQ) — e allertare sulla profondità e l’età della DLQ così i fallimenti vengano triagati invece di scadere silenziosamente. Un messaggio che scade dalla DLQ senza essere esaminato è un messaggio che hai deciso di perdere senza decidere di perderlo.
L’idempotenza è importante quanto ovunque altrove: i duplicati possono verificarsi; un messaggio che viene redriven da una DLQ back alla coda principale è, funzionalmente, una consegna duplicata. Se il tuo consumer non è sicuro da eseguire due volte sullo stesso messaggio, il redrive dalla DLQ può creare esattamente il bug dell’effetto collaterale duplicato che stavi cercando di evitare. Vedere Idempotenza nei Sistemi Distribuiti che Funzionano Davvero per i pattern lato consumer che rendono il redrive sicuro.
Strategie di Riprova
Far uscire correttamente un messaggio dalla DLQ è la sua propria disciplina, separata dal farlo entrare.
- Correggere prima la causa radice. Il deploy della correzione del consumer prima della riprova fa la differenza tra un recupero pulito e il re-velenizzare la coda con lo stesso fallimento una seconda volta.
- Redrive deliberatamente, non automaticamente. SQS supporta una funzione redrive-to-source che sposta i messaggi back alla loro coda originale (o altra destinazione) su richiesta; RabbitMQ e Kafka richiedono di costruire il consumer o gli strumenti equivalenti da soli. In ogni caso, trattare la riprova come un’azione attivata dall’operatore con un record di cosa è stato riprovato e quando.
- Preservare l’ordinamento dove conta. Per Kafka, l’argomento dead-letter dovrebbe avere almeno quanti partizioni ha il topic sorgente e dovrebbe conservare la chiave del messaggio originale, così che i messaggi riprovati atterrino back sulla partizione corretta e preservino l’ordinamento per chiave.
- Limitare i tentativi di riprova. Un messaggio che fallisce di nuovo dopo un ciclo di fix-and-replay non è transitorio: instradalo a un archivio permanente (una tabella del database, un bucket di object storage) invece di farlo circolare attraverso la DLQ indefinitamente. I documenti stessi di RabbitMQ avvertono che un messaggio dead-letterizzato può essere instradato tra le code solo un numero limitato di volte (16) prima che il further dead-lettering basato su TTL sia disabilitato.
- Non lasciare mai una DLQ dead-letterizzare sé stessa. Se la tua DLQ ha il suo
x-dead-letter-exchange(RabbitMQ) o la sua politica di redrive (SQS) puntata back alla stessa catena, un fallimento di riprova può creare un loop infinito. Mantenere vuota la configurazione dead-letter della DLQ, oppure puntarla a un archivio strettamente terminale. - Allertare sul volume, non solo sulla presenza. Un singolo messaggio in una DLQ è un dato; un picco improvviso è un incidente. Collegare la profondità della DLQ e l’età del messaggio allo stesso pipeline di allertamento che usi per tutto il resto — vedere Design Moderno dei Sistemi di Allertamento per i Team di Observability per pratiche di routing e riduzione del rumore che si applicano direttamente agli allarmi DLQ.
Se il tuo workflow coinvolge processi multi-step, a lunga esecuzione piuttosto che singoli messaggi, lo stesso pensiero dead-letter si applica al livello di workflow — la logica di compensazione di una saga ha bisogno della stessa disciplina “quarantena, ispeziona, decidi” quando un passo fallisce permanentemente invece che transitoriamente. E quando gli eventi stessi originano da una scrittura nel database, il pattern transactional outbox già costruisce la gestione dead-letter nel worker di relay, così il pattern si presenta un livello prima del broker.
Dove si insercono le DLQ nel quadro più ampio
Una dead-letter queue non fa sì che i fallimenti spariscano — rende i fallimenti sopravvissibili e revisionabili invece che silenziosi. Funziona meglio insieme alle riprove con backoff per il caso transitorio, consumer idempotenti così il redrive sia sicuro, e un circuit breaker così una dipendenza in difficoltà non inondi la coda principale (e, alla fine, la DLQ) con lo stesso fallimento migliaia di volte. Trattare la soglia DLQ, la ritenzione e l’allertamento come decisioni di configurazione di prima classe, non come default che lasci intatti, e i dead letters diventano uno strumento diagnostico invece che un luogo dove i dati scompaiono silenziosamente.