<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Viktor</title>
    <description>The latest articles on DEV Community by Viktor (@viktorbaumann).</description>
    <link>https://dev.to/viktorbaumann</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149913%2Ff56eae03-caa5-47f3-9235-4831bb377fc1.png</url>
      <title>DEV Community: Viktor</title>
      <link>https://dev.to/viktorbaumann</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/viktorbaumann"/>
    <language>en</language>
    <item>
      <title>Replay only some RabbitMQ dead letters: by exception, header or routing key</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:58:30 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/replay-only-some-rabbitmq-dead-letters-by-exception-header-or-routing-key-3goa</link>
      <guid>https://dev.to/viktorbaumann/replay-only-some-rabbitmq-dead-letters-by-exception-header-or-routing-key-3goa</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/replay-some-dead-letters-rabbitmq.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The tax service was down for twenty minutes and is back. &lt;code&gt;orders.dlq&lt;/code&gt; holds 230 messages: 41 of them failed with &lt;code&gt;TaxServiceUnavailableException&lt;/code&gt;, the rest are missing orders and declined payments that a replay would only send round again. RabbitMQ has no "move the messages where header X equals Y": a queue hands out messages from the head, one after the other, and the management UI's "Move messages" and a shovel take all of them. Here is how to take only the 41, measured on RabbitMQ 4.3.6 with classic and quorum queues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop that never ends
&lt;/h2&gt;

&lt;p&gt;The obvious script reads a message, checks it, and puts the ones it does not want straight back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# do not do this
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A message requeued this way goes back to the head of the queue, where it was. The next &lt;code&gt;basic_get&lt;/code&gt; returns the same message again, and the script spins on the first unwanted message forever. Classic and quorum queues behave the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hold the ones you skip
&lt;/h2&gt;

&lt;p&gt;The way through the queue is to keep the skipped messages unacknowledged while you read on. As long as a delivery is outstanding on the channel, &lt;code&gt;basic_get&lt;/code&gt; hands out the next message. At the end one &lt;code&gt;basic_nack&lt;/code&gt; gives all of them back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;

&lt;span class="n"&gt;QUEUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DEATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-first-death-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-last-death-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-delivery-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# your filter; Spring's RepublishMessageRecoverer writes x-exception-*
&lt;/span&gt;    &lt;span class="n"&gt;exc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-exception-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TaxServiceUnavailableException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# dead-lettered by the broker
&lt;/span&gt;        &lt;span class="n"&gt;death&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routing-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# republished by Spring's recoverer
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-original-exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-original-routingKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BlockingConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pika&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;URLParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm_delivery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# 1.
&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUEUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_count&lt;/span&gt;  &lt;span class="c1"&gt;# 2.
&lt;/span&gt;&lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUEUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 3.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# 4. hold it, unacked
&lt;/span&gt;        &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routing_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                         &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEATH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routing_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# raises if not routed
&lt;/span&gt;        &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# only after the confirm
&lt;/span&gt;        &lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 5. hand the rest back
&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We ran it against a dead-letter queue with nine messages: six dead-lettered by the broker, three republished by a Spring-style recoverer without &lt;code&gt;x-death&lt;/code&gt;. It moved the three that matched to the work queue, without death headers, and left the other six in the dead-letter queue in their original order. Same result on a classic and a quorum queue. With a filter that raised an exception on the fifth message, the one message already moved stayed moved and the other eight were back in the dead-letter queue, their delivery count unchanged.&lt;/p&gt;

&lt;p&gt;What each numbered comment is for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirms.&lt;/strong&gt; With &lt;code&gt;confirm_delivery()&lt;/code&gt;, pika's &lt;code&gt;basic_publish&lt;/code&gt; waits for the broker and raises if the message was not taken; with &lt;code&gt;mandatory=True&lt;/code&gt; it also raises when no queue is bound for the routing key. Ack the original only after that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A snapshot of the length.&lt;/strong&gt; Read at most as many messages as the queue held when you started. Without it, a queue that keeps receiving dead letters while you read never runs empty, and the script never stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-ack.&lt;/strong&gt; With &lt;code&gt;auto_ack=True&lt;/code&gt; a skipped message is gone the moment you look at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold, do not nack.&lt;/strong&gt; The skipped message stays unacknowledged on this channel until step 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One nack at the end, also when something fails.&lt;/strong&gt; &lt;code&gt;basic_nack(0, multiple=True)&lt;/code&gt; returns every delivery still outstanding on the channel, in the &lt;code&gt;finally&lt;/code&gt; block so that an exception in your filter or a failed publish hands the messages back the same way. The skipped messages go back to the head of the queue in their old order.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Afterwards every message that was skipped is marked &lt;code&gt;redelivered&lt;/code&gt;. That is harmless in a dead-letter queue, but worth knowing if a consumer treats the flag as suspicious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just close the connection?
&lt;/h2&gt;

&lt;p&gt;Closing the channel also returns every unacknowledged message, and many scripts rely on it. On a classic queue that is fine. On a quorum queue it is not the same thing as a nack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the skipped messages go back (quorum queue, 4.3.6)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;x-delivery-count&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;29 scans of one message, default delivery limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;basic_nack(requeue=True)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;td&gt;message still there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;connection closed with the message unacked&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+1 per close&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;message gone at scan 22&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RabbitMQ 4 gives every quorum queue a delivery limit of 20. A return by channel close counts as a failed delivery; an explicit nack does not. Twenty-one scans that end with &lt;code&gt;conn.close()&lt;/code&gt;, or twenty-one crashes of the script, and the message is dropped, or dead-lettered further if the dead-letter queue has a dead-letter exchange of its own. On a quorum dead-letter queue that is fed by a quorum queue the margin is smaller still, because the message arrives with the deliveries it already used up: in our test a single reject on the work queue brought it in with &lt;code&gt;x-delivery-count&lt;/code&gt; 1, and the &lt;a href="https://warrenops.io/blog/rabbitmq-quorum-dlq-delivery-limit.html" rel="noopener noreferrer"&gt;quorum DLQ article&lt;/a&gt; shows a poison message arriving with 21. So end every scan with the nack, in a &lt;code&gt;finally&lt;/code&gt; block as above, and never leave the messages to the channel close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filters that work
&lt;/h2&gt;

&lt;p&gt;What you can filter on depends on who put the message into the dead-letter queue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead-lettered by the broker&lt;/strong&gt; (reject, expiry, queue length): &lt;code&gt;x-death[0]&lt;/code&gt; has &lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;queue&lt;/code&gt;, &lt;code&gt;exchange&lt;/code&gt; and &lt;code&gt;routing-keys&lt;/code&gt;. The exception is not there, because the broker never saw it. Your own publish headers are kept, so a header like &lt;code&gt;x-tenant&lt;/code&gt; or &lt;code&gt;x-order-type&lt;/code&gt; works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Republished by the consumer&lt;/strong&gt; (Spring's &lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;, MassTransit's &lt;code&gt;_error&lt;/code&gt; queues): the exception sits in the headers, &lt;code&gt;x-exception-type&lt;/code&gt; and &lt;code&gt;x-exception-message&lt;/code&gt; for Spring, &lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt; for MassTransit, and there is no &lt;code&gt;x-death&lt;/code&gt;. Spring keeps the route in &lt;code&gt;x-original-exchange&lt;/code&gt; and &lt;code&gt;x-original-routingKey&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The body.&lt;/strong&gt; Possible, but parse defensively: some dead letters are dead because the body does not parse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exception messages usually contain ids ("Order 4711 not found"). Compare the exception type, or the message with digits replaced, not the full text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or split them before they mix
&lt;/h2&gt;

&lt;p&gt;If the same question comes up every week, let the broker sort the dead letters as they arrive. A dead-lettered message keeps its routing key unless the queue sets &lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;, so a topic exchange as dead-letter exchange can send each kind to its own queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq.created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq.created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq.cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq.cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.work&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-dead-letter-exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then "replay the created orders" is "replay one queue", and "Move messages" or a shovel can do it. Two cautions from the test: a catch-all binding with &lt;code&gt;#&lt;/code&gt; next to the specific ones receives a copy of every message, so the two queues overlap; and a routing key the bindings do not cover is dropped silently. Bind every key you use, or put an alternate exchange on the dead-letter exchange. Splitting works for routing keys and your own headers (a headers exchange), not for the exception, which the broker does not know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never nack a skipped message right away; hold it, and nack everything still held at the end.&lt;/li&gt;
&lt;li&gt;End the scan with that nack in a &lt;code&gt;finally&lt;/code&gt; block, not with a connection close, above all on quorum queues.&lt;/li&gt;
&lt;li&gt;Read at most the queue length you saw at the start.&lt;/li&gt;
&lt;li&gt;Confirms and &lt;code&gt;mandatory=True&lt;/code&gt; on the publish, ack after the confirm.&lt;/li&gt;
&lt;li&gt;Filter on what is actually there: &lt;code&gt;x-death&lt;/code&gt; for broker dead letters, exception headers for republished ones.&lt;/li&gt;
&lt;li&gt;If you filter by the same thing every time, split the dead-letter queue by routing key instead.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren does this selection in the browser. On a dead-letter queue's page you group the messages by exception, reason, queue, routing key or number of deaths and click a group to narrow the table. Then you replay the ticked rows, or every message in the whole queue that matches a reason or a search text; Warren checks that with a dry run first, so the replay takes exactly the messages you saw. On the Failures page one row is one exception across all dead-letter queues, and Replay on that row replays it in every queue you tick. Each message goes to its original route with confirms, and the ones you did not pick stay where they were. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/replay.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>python</category>
      <category>messaging</category>
    </item>
    <item>
      <title>Replay one message first: a canary for RabbitMQ dead letters</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:00:04 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/replay-one-message-first-a-canary-for-rabbitmq-dead-letters-1f3a</link>
      <guid>https://dev.to/viktorbaumann/replay-one-message-first-a-canary-for-rabbitmq-dead-letters-1f3a</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/replay-one-message-first.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is merged, the pipeline is green, and 143 messages wait in &lt;code&gt;orders.dlq&lt;/code&gt;. Replaying all of them at once is a bet: that the new version runs on every consumer replica, that the fix covers these payloads and not only the one in the ticket, and that the consumer does not fail on something else further down. If the bet is lost, 143 messages die a second time, the alerts fire again, and the dead-letter queue now mixes old failures with new ones. Sending one message first and waiting a moment costs a couple of minutes and settles the question for almost nothing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The idea as a video (canary replay and the incident report, captions, no sound needed):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pN8LybiJv8M" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you send anything
&lt;/h2&gt;

&lt;p&gt;A canary only means something if its message is processed during the wait. Check two things on the work queue first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A consumer is attached.&lt;/strong&gt; &lt;code&gt;rabbitmqctl list_queues name messages consumers&lt;/code&gt; or the queue's page in the management UI. With no consumer the canary sits in the queue and every wait "passes".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The work queue is not backed up.&lt;/strong&gt; If 20,000 messages are ahead of the canary, it is processed after the wait, not during it. Wait until the backlog is gone, or publish the canary later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then pick the canary from the failure you mean to fix. If the dead-letter queue holds two different exceptions, one canary passing says nothing about the other; send one per exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send one message, tagged
&lt;/h2&gt;

&lt;p&gt;The replay is the same as for a whole batch, stopped after the first message: read it from the dead-letter queue, publish it to its original route with a header that names this replay, wait for the publisher confirm, then acknowledge the original.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;

&lt;span class="n"&gt;DEATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-first-death-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-last-death-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-delivery-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-acquired-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;replay_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm_delivery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;death&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# broker dead-lettering; Spring or MassTransit error queues keep the route elsewhere
&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEATH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-replay-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routing-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# confirm_delivery made the publish wait for the broker
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your own headers survive dead-lettering on classic and quorum queues, so if the canary dies again it comes back carrying &lt;code&gt;x-replay-id&lt;/code&gt;, together with a fresh &lt;code&gt;x-death&lt;/code&gt;. That pair is what tells its return apart from a new failure that arrived in the meantime. The details are in &lt;a href="https://warrenops.io/blog/did-the-replay-work.html" rel="noopener noreferrer"&gt;Did the replay work?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long to wait
&lt;/h2&gt;

&lt;p&gt;Long enough for the consumer's whole retry chain to run out, plus the time to reach the canary. The chain is usually the larger part:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retry set-up&lt;/th&gt;
&lt;th&gt;When a failing message reaches the DLQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No retries, &lt;code&gt;basic.reject&lt;/code&gt; with &lt;code&gt;requeue=false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;within a second of delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring &lt;code&gt;RetryTemplate&lt;/code&gt; / &lt;code&gt;@Retryable&lt;/code&gt;, 3 attempts, backoff 1 s × 2&lt;/td&gt;
&lt;td&gt;after about 3 s (1 + 2), plus processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker-side retry: reject into a wait queue whose TTL dead-letters back, 5 rounds of 30 s&lt;/td&gt;
&lt;td&gt;after about 2.5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MassTransit &lt;code&gt;UseDelayedRedelivery&lt;/code&gt; with 5, 15, 30 minutes&lt;/td&gt;
&lt;td&gt;after about 50 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the numbers off your own configuration. Two minutes covers in-process retries with short backoffs; a delayed-redelivery set-up needs a canary that waits an hour, or a canary sent before lunch and the rest afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch for it
&lt;/h2&gt;

&lt;p&gt;During the wait nothing else is replayed, so the dead-letter queue changes only through new failures and through the canary coming back. Check its depth; when it grows, look at what was added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;came_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth_before&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_count&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;depth_before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="c1"&gt;# returns arrive at the tail: read the whole queue and put everything back
&lt;/span&gt;    &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-replay-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take &lt;code&gt;depth_before&lt;/code&gt; right after the canary left the queue, and call this every few seconds until the wait is over. Reading and putting back marks messages as redelivered. On a quorum dead-letter queue with a delivery limit, check &lt;a href="https://warrenops.io/blog/rabbitmq-quorum-dlq-delivery-limit.html" rel="noopener noreferrer"&gt;what a requeue costs&lt;/a&gt; on your RabbitMQ version before you peek at all.&lt;/p&gt;

&lt;p&gt;If your consumer sends failures to a different queue than the one the message came from (a parking queue after the last retry, a Spring &lt;code&gt;.dlq&lt;/code&gt; next to a broker DLQ), watch that queue too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The canary came back.&lt;/strong&gt; Stop. The other 142 stay where they are, which is exactly where you want them while you read the new exception on the canary. Nothing is mixed up: one message died twice, and it says so in its headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It did not come back within the wait.&lt;/strong&gt; Replay the rest, ideally throttled, so a consumer that has only just recovered is not flooded. Tag them with the same replay id and keep looking for returns over the next hours, as for any replay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New messages arrived but none is the canary.&lt;/strong&gt; Something else is failing right now. Find out what before the 142 join them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a passed canary proves nothing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The consumer acknowledged and dropped it.&lt;/strong&gt; "Not back" means not seen in a dead-letter queue. If the consumer logs a business id, check that the canary's id shows up as processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The canary was not representative.&lt;/strong&gt; It was the one payload the fix was written for. If the batch holds several customers, tenants or message versions, a canary from each is cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure depends on load.&lt;/strong&gt; A canary does not hit a rate limit, a connection pool or a lock that 142 messages at full speed do. That is what throttling the rest is for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wait was shorter than the retry chain.&lt;/strong&gt; The canary is still in its third retry when the rest goes out. Check the table above against your own settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A consumer is attached to the work queue and it is not backed up.&lt;/li&gt;
&lt;li&gt;One canary per exception, tagged with a replay id, death headers stripped.&lt;/li&gt;
&lt;li&gt;Wait longer than the consumer's full retry chain.&lt;/li&gt;
&lt;li&gt;Back in the queue with your replay id and a new &lt;code&gt;x-death&lt;/code&gt;: stop. Not back: replay the rest, throttled.&lt;/li&gt;
&lt;li&gt;Keep watching for returns after the rest went out.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren's replay dialog has a &lt;strong&gt;Canary&lt;/strong&gt; option: it replays the first message of the selection alone, watches the source queue for it during the wait (2 minutes by default, 5 seconds to 30 minutes), and replays the rest only if it has not died back by then, at the rate you set. The replay's page says "Canary out" with the time the wait ends, then "Canary passed" or "Canary died again"; a canary that died stops the replay and the audit log records why. The Failures page offers the same per exception, and its incident report lists every replay of that failure with how its canary went. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/replay.html#one-message-first-the-canary" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>messaging</category>
      <category>sre</category>
    </item>
    <item>
      <title>Dead letters in the RabbitMQ management UI: what it can do, and where it bites</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Thu, 08 Oct 2026 07:32:38 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/dead-letters-in-the-rabbitmq-management-ui-what-it-can-do-and-where-it-bites-2okn</link>
      <guid>https://dev.to/viktorbaumann/dead-letters-in-the-rabbitmq-management-ui-what-it-can-do-and-where-it-bites-2okn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/rabbitmq-management-ui-dead-letters.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every RabbitMQ operator has the management UI open, and when a dead-letter queue fills up, it is the first place anyone looks. Each queue page has a small corner for messages: Get messages, Move messages, Publish message and Purge. It is enough for a lot of days. It also has a few defaults and side effects that are easy to miss in the middle of an incident. Here is what each button does, measured on RabbitMQ 4.3.6 with classic and quorum queues.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same task in the management UI and in Warren, side by side (2:02, captions, no sound needed):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/HlXmCdB2hGk" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Get messages: four ack modes, one safe default
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Get messages&lt;/em&gt; fetches messages with &lt;code&gt;basic.get&lt;/code&gt; and then settles them according to the &lt;strong&gt;Ack Mode&lt;/strong&gt; you pick. The form starts at one message, and payloads are cut at 50,000 bytes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ack Mode&lt;/th&gt;
&lt;th&gt;What happens to the message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nack message requeue true (default)&lt;/td&gt;
&lt;td&gt;put back, marked &lt;code&gt;redelivered&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic ack&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;removed&lt;/strong&gt; from the queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reject requeue true&lt;/td&gt;
&lt;td&gt;put back, marked &lt;code&gt;redelivered&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reject requeue false&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;removed&lt;/strong&gt;, or dead-lettered again if the queue has a dead-letter exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two "put back" modes look the same. On a classic queue they are: both put the message back where it was. On a quorum queue they are not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quorum DLQ, three messages m1 m2 m3, then one Get messages of m1:
  Nack message requeue true   -&amp;gt; m1 m2 m3, x-delivery-count of m1 unchanged
  Reject requeue true         -&amp;gt; m2 m3 m1, x-delivery-count of m1 + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured on 4.3.6, also with plain AMQP: &lt;code&gt;basic.nack&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt; does not count against a quorum queue's delivery limit, &lt;code&gt;basic.reject&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt; does, and it moves the message to the back. Every &lt;em&gt;Reject requeue true&lt;/em&gt; spends one delivery of the message's budget.&lt;/p&gt;

&lt;p&gt;The two "removed" modes are the dangerous ones on a dead-letter queue. A DLQ usually has no dead-letter exchange of its own, so &lt;em&gt;Reject requeue false&lt;/em&gt; does not move the message anywhere: it is gone, exactly like &lt;em&gt;Automatic ack&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the default.&lt;/strong&gt; &lt;em&gt;Nack message requeue true&lt;/em&gt; is the only mode that neither removes a message nor costs it a delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quorum dead-letter queues on RabbitMQ 4: look before you peek
&lt;/h2&gt;

&lt;p&gt;Even the default has one trap. Since RabbitMQ 4.0 every quorum queue has a delivery limit of 20 unless you set one, and a message that a quorum work queue dead-letters for &lt;code&gt;delivery_limit&lt;/code&gt; arrives in a quorum DLQ with its count already at 21. On 4.3.6, a message whose count is above the queue's limit is dropped the moment it is put back, and &lt;em&gt;Get messages&lt;/em&gt; with requeue puts it back. One look and it is gone.&lt;/p&gt;

&lt;p&gt;Before anyone opens &lt;em&gt;Get messages&lt;/em&gt; on a quorum DLQ, take the limit off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl set_policy dlq-no-limit &lt;span class="s1"&gt;'\.dlq$'&lt;/span&gt; &lt;span class="s1"&gt;'{"delivery-limit": -1}'&lt;/span&gt; &lt;span class="nt"&gt;--apply-to&lt;/span&gt; quorum_queues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://warrenops.io/blog/rabbitmq-quorum-dlq-delivery-limit.html" rel="noopener noreferrer"&gt;quorum article&lt;/a&gt; has the measurements and two other fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading why a message died
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Get messages&lt;/em&gt; shows the headers of each message as nested tables. The &lt;code&gt;x-death&lt;/code&gt; table is the incident report: the queue the message died in, the reason (&lt;code&gt;rejected&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;, &lt;code&gt;maxlen&lt;/code&gt;, &lt;code&gt;delivery_limit&lt;/code&gt;), how often, and the exchange and routing keys it was originally published with. The &lt;a href="https://warrenops.io/blog/reading-the-x-death-header.html" rel="noopener noreferrer"&gt;x-death article&lt;/a&gt; goes through it field by field.&lt;/p&gt;

&lt;p&gt;What the UI does not do is look across messages. With five messages you read them one by one. With 300 messages and three different exceptions, you are scrolling a page of raw payloads and nested tables, looking for the dozen that are different. There is no search inside a queue and no grouping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move messages: one queue in, one queue out
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Move messages&lt;/em&gt; appears on the queue page once the &lt;code&gt;rabbitmq_shovel&lt;/code&gt; and &lt;code&gt;rabbitmq_shovel_management&lt;/code&gt; plugins are enabled. It asks for one thing, a destination queue, and creates a dynamic shovel named &lt;code&gt;Move from &amp;lt;queue&amp;gt;&lt;/code&gt; with these settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;src&lt;/span&gt;-&lt;span class="n"&gt;delete&lt;/span&gt;-&lt;span class="n"&gt;after&lt;/span&gt;: &lt;span class="n"&gt;queue&lt;/span&gt;-&lt;span class="n"&gt;length&lt;/span&gt;    &lt;span class="c"&gt;# move what is there now, then delete the shovel
&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;-&lt;span class="n"&gt;mode&lt;/span&gt;:         &lt;span class="n"&gt;on&lt;/span&gt;-&lt;span class="n"&gt;confirm&lt;/span&gt;      &lt;span class="c"&gt;# a message leaves the source once the destination has it
&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;-&lt;span class="n"&gt;queue&lt;/span&gt;:       &amp;lt;&lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;typed&lt;/span&gt;&amp;gt;
&lt;span class="n"&gt;dest&lt;/span&gt;-&lt;span class="n"&gt;add&lt;/span&gt;-&lt;span class="n"&gt;forward&lt;/span&gt;-&lt;span class="n"&gt;headers&lt;/span&gt;: &lt;span class="n"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes it a safe move: nothing is lost on the way, and the shovel removes itself when it is done. What it does not do matters more for dead letters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything goes to one queue.&lt;/strong&gt; A DLQ that collects from several queues or routing keys cannot be sent back to where each message came from. The original exchange and routing key are in each message's &lt;code&gt;x-death&lt;/code&gt;; &lt;em&gt;Move messages&lt;/em&gt; does not read them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All or nothing.&lt;/strong&gt; It moves every message that is in the queue when it starts. There is no selection: the twelve poison messages go along with the 288 that only need a second try.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The death headers travel along.&lt;/strong&gt; In our test the moved messages arrived with &lt;code&gt;x-death&lt;/code&gt;, &lt;code&gt;x-first-death-*&lt;/code&gt;, &lt;code&gt;x-last-death-*&lt;/code&gt; and, from a quorum queue, the old &lt;code&gt;x-delivery-count&lt;/code&gt; as a plain header. A consumer that reads the &lt;code&gt;x-death&lt;/code&gt; count to decide when to give up sees the old count on arrival. If yours does, strip the headers when you replay instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pacing.&lt;/strong&gt; The shovel moves as fast as the broker lets it, with up to 1,000 messages in flight. A consumer that just recovered gets the whole backlog at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the destination is the one queue the messages should go to, and you trust all of them, it is the right button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish message: replaying one message by hand
&lt;/h2&gt;

&lt;p&gt;To send a single message back to its route, people copy the payload from &lt;em&gt;Get messages&lt;/em&gt; into &lt;em&gt;Publish message&lt;/em&gt; on the exchange. Two things get lost on the way. The form sets properties and headers you type in, and its own help says it: "Only long string headers can be set here." Numbers, booleans and nested headers do not survive. And the original stays in the DLQ until you remove it with a second, destructive &lt;em&gt;Get messages&lt;/em&gt;. Copy, publish, check, then remove, in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Purge: final
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Purge&lt;/em&gt; deletes every message in the queue. The UI asks once, in its own words: "Are you sure? Messages cannot be recovered after purging." There is no copy, no list of what was in the queue and no record of who did it. If a queue might hold anything you need later, export it first (&lt;em&gt;Get messages&lt;/em&gt; with &lt;em&gt;Nack message requeue true&lt;/em&gt; and a high count, then save the page, or a script).&lt;/p&gt;

&lt;h2&gt;
  
  
  Who did what
&lt;/h2&gt;

&lt;p&gt;The management UI keeps no record of messages fetched, moved, published or purged. If the postmortem asks who replayed what at 3:12, the answer is in someone's shell history or nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Get messages&lt;/em&gt;: keep the default, &lt;em&gt;Nack message requeue true&lt;/em&gt;. &lt;em&gt;Reject requeue true&lt;/em&gt; costs a delivery on a quorum queue; &lt;em&gt;Automatic ack&lt;/em&gt; and &lt;em&gt;Reject requeue false&lt;/em&gt; remove the message.&lt;/li&gt;
&lt;li&gt;Quorum DLQ on RabbitMQ 4: set &lt;code&gt;delivery-limit: -1&lt;/code&gt; before the first peek.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Move messages&lt;/em&gt;: one destination queue, all messages, death headers included. Fine for a straight move, wrong for "back to where each one came from".&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Publish message&lt;/em&gt;: string headers only; remove the original afterwards, not before.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Purge&lt;/em&gt;: export first if anything in the queue could matter.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren covers the message corner of the management UI for dead-letter work, and leaves configuration to it. It peeks with a requeue that costs nothing and asks before it reads a quorum queue where a peek could drop messages; it groups a DLQ by exception and searches it; it replays selected messages to the exchange and routing key in each one's &lt;code&gt;x-death&lt;/code&gt;, with the death headers stripped, throttled and stoppable; it keeps a copy of discarded and purged messages for seven days, and an audit log of who did what. Free for one broker. A side-by-side table is on &lt;a href="https://warrenops.io/blog/warren-vs-rabbitmq-management-ui.html" rel="noopener noreferrer"&gt;Warren vs. the RabbitMQ management UI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>messaging</category>
      <category>sre</category>
    </item>
    <item>
      <title>Did the replay work? Finding the RabbitMQ dead letters that come back</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:10:11 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/did-the-replay-work-finding-the-rabbitmq-dead-letters-that-come-back-hff</link>
      <guid>https://dev.to/viktorbaumann/did-the-replay-work-finding-the-rabbitmq-dead-letters-that-come-back-hff</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/did-the-replay-work.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is deployed, 143 messages go back from the dead-letter queue to their original exchange, the broker confirms every one of them, and the incident is closed. Ten minutes later the dead-letter queue holds 5 messages again. Are they five of the 143, or five new failures? Without a little preparation nobody can tell, because a publish confirm only says the broker took the message, not that the consumer processed it. Here is how to make a replay answer that question, measured on RabbitMQ 4.3.6.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The short version as a video (0:58, captions, no sound needed):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ql3hc0SXDJI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What a confirmed replay tells you
&lt;/h2&gt;

&lt;p&gt;A replay done right reads each message from the dead-letter queue, publishes it to its original route with publisher confirms, and acknowledges the original only after the confirm. That guarantees nothing is lost on the way. It says nothing about what happens next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the consumer processes the message, which is what you hoped;&lt;/li&gt;
&lt;li&gt;the consumer fails on it again, and it is dead-lettered back, because the fix did not cover this case;&lt;/li&gt;
&lt;li&gt;the message sits in the work queue because the consumer is not running yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The management UI's "Move messages", a shovel, or a script that loops &lt;code&gt;basic.get&lt;/code&gt; and &lt;code&gt;basic.publish&lt;/code&gt; all stop at the confirm. To see the second case you have to recognise a message when it comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tag what you replay
&lt;/h2&gt;

&lt;p&gt;Give every replayed message a header that names the replay, and remove the death headers so the message starts fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;

&lt;span class="n"&gt;DEATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-first-death-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-last-death-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-delivery-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-acquired-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;replay_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm_delivery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;
    &lt;span class="n"&gt;death&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# broker dead-lettering; Spring or MassTransit error queues keep the route elsewhere
&lt;/span&gt;    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEATH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-replay-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt;
    &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routing-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# confirm_delivery made the publish wait for the broker
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;message_id&lt;/code&gt; and the body as they are: they are what lets you match a returned message to the one you sent, also when two replays of the same message overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survives the second death
&lt;/h2&gt;

&lt;p&gt;We published a message with an application header, rejected it into a dead-letter queue, replayed it with &lt;code&gt;x-replay-id&lt;/code&gt; (once with the death headers stripped, once with them kept), and rejected it again. Classic and quorum queues behaved the same:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;After the second dead-lettering&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;your own headers (&lt;code&gt;x-app&lt;/code&gt;, &lt;code&gt;x-replay-id&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;kept&lt;/strong&gt;, unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-death&lt;/code&gt; when the replay stripped it&lt;/td&gt;
&lt;td&gt;a fresh entry, &lt;code&gt;count&lt;/code&gt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-death&lt;/code&gt; when the replay kept it&lt;/td&gt;
&lt;td&gt;also a fresh entry, &lt;code&gt;count&lt;/code&gt; 1: the broker does not continue a count a publisher sent along&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x-delivery-count&lt;/code&gt; (quorum)&lt;/td&gt;
&lt;td&gt;starts again at 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things follow. Application headers ride along through dead-lettering, so the replay id is still there when the message comes back. And &lt;code&gt;x-death&lt;/code&gt; cannot count replays: whatever the message went through before, after a republish RabbitMQ starts from scratch. Inside a broker-side retry loop (reject into a wait queue whose TTL dead-letters back to the work queue) the count does go up, 1, 2, 3, as before; it is only the republish that resets it. If you want to know how often a message has been replayed, count it in a header of your own, for example &lt;code&gt;x-replay-count&lt;/code&gt;, increased on every replay.&lt;/p&gt;

&lt;p&gt;A message that came back is therefore one that carries your replay id &lt;strong&gt;and&lt;/strong&gt; a death: a new &lt;code&gt;x-death&lt;/code&gt; from the broker, or, for consumers that republish failures themselves (Spring's &lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;, which copies the original headers), fresh exception headers. The id alone is not enough, because a replayed message still waiting in the work queue carries it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look for them
&lt;/h2&gt;

&lt;p&gt;Returned messages arrive at the tail of the dead-letter queue. A peek through the management API takes the head, so look early, while the queue is short, or read deep enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqadmin &lt;span class="nt"&gt;-f&lt;/span&gt; raw_json get &lt;span class="nv"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;orders.dlq &lt;span class="nv"&gt;ackmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ack_requeue_true &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;--arg&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPLAY_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'[.[] | select(.properties.headers["x-replay-id"] == $id
        and .properties.headers["x-death"] != null)] | length'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ack_requeue_true&lt;/code&gt; puts the messages back. They are marked redelivered afterwards, and on a quorum queue of RabbitMQ 3.13 every such peek counts as a delivery against the queue's delivery limit; on 4.x a requeue no longer counts.&lt;/p&gt;

&lt;p&gt;When to look: returns usually show up within minutes, because the consumer fails on the message as soon as it gets it. Look once after a few minutes and again after an hour or two. Failures that depend on time (a nightly job, a token that expires, a downstream that is only down at peak) need a second look the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;None came back.&lt;/strong&gt; Good, with a caveat: "not back" means not seen in a dead-letter queue. A consumer that acknowledges and drops a message it cannot handle looks the same. If your consumers log a business id, a spot check of five messages in the consumer log settles it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A few came back, all with the same exception.&lt;/strong&gt; The fix covered most cases but not this one. Group the returned messages by exception before replaying them again; replaying them unchanged produces the same result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most came back.&lt;/strong&gt; The fix is not deployed where the consumer runs, or the consumer fails on something the replay itself changed, for example a header your code relied on and the replay removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They come back again and again.&lt;/strong&gt; A message replayed three times without success is not waiting for the next deploy. Park it and read it. Your own replay counter tells you when that point is reached.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every replay gets an id, written into each message as a header.&lt;/li&gt;
&lt;li&gt;Strip the death headers on replay; count replays in a header of your own, because &lt;code&gt;x-death&lt;/code&gt; restarts after a republish.&lt;/li&gt;
&lt;li&gt;A message with your replay id and a new death came back. The id alone does not mean that.&lt;/li&gt;
&lt;li&gt;Look after a few minutes, after an hour or two, and the next day.&lt;/li&gt;
&lt;li&gt;Group what came back by exception before you replay it again.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren does this bookkeeping for every replay: each message carries &lt;code&gt;x-warren-replay-id&lt;/code&gt;, every read of a queue notices messages that died again, and Warren reads the source queue itself 5 minutes, 30 minutes, 2 hours and a day after the replay. The replay's page then says "5 of 143 replayed messages died again: 5 back in orders.dlq" and marks them; the audit log shows "5 back" next to the count. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/replay.html#did-it-help" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>messaging</category>
      <category>sre</category>
    </item>
    <item>
      <title>MassTransit on RabbitMQ: what ends up in _error and _skipped, and how to move it back</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:00:49 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/masstransit-on-rabbitmq-what-ends-up-in-error-and-skipped-and-how-to-move-it-back-56il</link>
      <guid>https://dev.to/viktorbaumann/masstransit-on-rabbitmq-what-ends-up-in-error-and-skipped-and-how-to-move-it-back-56il</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/masstransit-rabbitmq-error-queue.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A consumer throws, MassTransit retries, and after the last attempt the message is gone from the endpoint. It is not lost: it sits in a queue next to it, &lt;code&gt;shipping-label_error&lt;/code&gt;, with the exception in its headers. RabbitMQ's dead-lettering played no part in that, which is why the usual DLQ advice (&lt;code&gt;x-death&lt;/code&gt;, dead-letter exchanges) does not quite fit. Here is what MassTransit does, what the headers say, and how to put the messages back once the bug is fixed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The short version as a video (1:17, captions, no sound needed):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/TJjsFJAOSB8" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The topology MassTransit declares
&lt;/h2&gt;

&lt;p&gt;For a receive endpoint &lt;code&gt;shipping-label&lt;/code&gt; on RabbitMQ, MassTransit declares a queue &lt;code&gt;shipping-label&lt;/code&gt; and an exchange of the same name bound to it. Message types get exchanges of their own (&lt;code&gt;Shop.Contracts:CreateShippingLabel&lt;/code&gt;), bound to the endpoint's exchange when a consumer on that endpoint handles the type. Two more pairs appear the first time they are needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shipping-label_error&lt;/code&gt;&lt;/strong&gt;: messages whose consumer threw, after all retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shipping-label_skipped&lt;/code&gt;&lt;/strong&gt;: messages that arrived at the endpoint but that no consumer there handles, typically after a consumer was removed or renamed while publishers kept sending.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMassTransit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddConsumer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateShippingLabelConsumer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UsingRabbitMq&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rabbitmq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMq:Password"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReceiveEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipping-label"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigureConsumer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateShippingLabelConsumer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MassTransit publishes the message itself into the error queue, through the &lt;code&gt;shipping-label_error&lt;/code&gt; exchange, and acknowledges the original. The broker never dead-lettered anything, so there is no &lt;code&gt;x-death&lt;/code&gt; header and no dead-letter exchange on the queue. Tools and scripts that look for &lt;code&gt;x-death&lt;/code&gt; find nothing to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the headers tell you
&lt;/h2&gt;

&lt;p&gt;The error queue keeps MassTransit's JSON envelope as the body (&lt;code&gt;application/vnd.masstransit+json&lt;/code&gt;: &lt;code&gt;messageId&lt;/code&gt;, &lt;code&gt;conversationId&lt;/code&gt;, &lt;code&gt;sourceAddress&lt;/code&gt;, &lt;code&gt;destinationAddress&lt;/code&gt;, &lt;code&gt;messageType&lt;/code&gt; and your &lt;code&gt;message&lt;/code&gt;) and adds headers that answer most of the postmortem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MT-Reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fault&lt;/code&gt; for the error queue, &lt;code&gt;skip&lt;/code&gt; for the skipped queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the exception, e.g. &lt;code&gt;Shop.Shipping.CarrierUnavailableException&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MT-Fault-Message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;its message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MT-Fault-StackTrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the stack trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MT-Fault-ConsumerType&lt;/code&gt;, &lt;code&gt;MT-Fault-MessageType&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;which consumer failed on which message type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MT-Fault-RetryCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;how many retries ran before it gave up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MT-Fault-Timestamp&lt;/code&gt;, &lt;code&gt;MT-Host-*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;when, and on which machine and MassTransit version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Group the error queue by &lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt; and the incident usually explains itself: five &lt;code&gt;CarrierUnavailableException&lt;/code&gt;s from one outage of the carrier, two &lt;code&gt;InvalidOperationException&lt;/code&gt;s that are real bugs in the data. Those two groups want different treatment.&lt;/p&gt;

&lt;p&gt;MassTransit also publishes a &lt;code&gt;Fault&amp;lt;CreateShippingLabel&amp;gt;&lt;/code&gt; event when this happens. Subscribing to it is the cleanest way to alert from inside your code; watching the depth of &lt;code&gt;*_error&lt;/code&gt; queues is the way to alert from outside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry, redelivery, and when a message reaches the error queue
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReceiveEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipping-label"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// second-level: back to the broker, delivered again after 5, 15 and 30 minutes&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseDelayedRedelivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intervals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="c1"&gt;// first-level: in memory, a few quick attempts&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseMessageRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intervals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigureConsumer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateShippingLabelConsumer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UseMessageRetry&lt;/code&gt; retries inside the consumer, in memory, while the message stays unacknowledged; keep it to seconds. &lt;code&gt;UseDelayedRedelivery&lt;/code&gt; hands the message back to the broker and has it delivered again later; it needs a scheduler (&lt;code&gt;cfg.UseDelayedMessageScheduler()&lt;/code&gt; with RabbitMQ's delayed-message-exchange plugin) and counts the rounds in the header &lt;code&gt;MT-Redelivery-Count&lt;/code&gt;. Only when both are used up does the message go to &lt;code&gt;_error&lt;/code&gt;, with &lt;code&gt;MT-Fault-RetryCount&lt;/code&gt; saying how far it got.&lt;/p&gt;

&lt;p&gt;Two consequences for a replay later. A transient failure (a carrier down for twenty minutes) should rarely reach the error queue if the redelivery intervals outlast typical outages; if it does, the intervals were too short. And a message that still carries &lt;code&gt;MT-Redelivery-Count&lt;/code&gt; from its first life starts the next one with part of its budget spent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the messages back
&lt;/h2&gt;

&lt;p&gt;The bug is fixed, the carrier is back. The messages belong in &lt;code&gt;shipping-label&lt;/code&gt; again. What to get right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Send them to the endpoint's exchange&lt;/strong&gt;, &lt;code&gt;shipping-label&lt;/code&gt;, routing key empty. It is bound to the queue, so the message arrives exactly where MassTransit would deliver it. Publishing to the message type's exchange instead would reach every endpoint that subscribes to the type, which is a second delivery for all of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave the body alone.&lt;/strong&gt; MassTransit dispatches on the envelope's &lt;code&gt;messageType&lt;/code&gt;. Unwrap the JSON to "simplify" it and the consumer no longer recognises the message, which then lands in &lt;code&gt;_skipped&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the fault and redelivery headers&lt;/strong&gt;: &lt;code&gt;MT-Reason&lt;/code&gt;, &lt;code&gt;MT-Fault-*&lt;/code&gt; and &lt;code&gt;MT-Redelivery-Count&lt;/code&gt;, so the message starts with a full retry budget and a later fault is not mixed up with this one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm before you acknowledge&lt;/strong&gt;, and go slowly if the consumer just recovered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped messages are different&lt;/strong&gt;: replaying them to the same endpoint skips them again. Deploy the consumer for their type first, or send them to the endpoint that has it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The management UI's "Move messages" (shovel plugin) covers point 1 for a whole queue, and nothing else: no selection by exception, headers untouched, no record of what was moved. A small tool with RabbitMQ.Client 7 does all of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RabbitMQ.Client 7: with confirmation tracking, BasicPublishAsync returns once the broker confirmed&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConnectionFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rabbitUri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateConnectionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error-replay"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateChannelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateChannelOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publisherConfirmationsEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;publisherConfirmationTrackingEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;errorQueue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shipping-label_error"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errorQueue&lt;/span&gt;&lt;span class="p"&gt;[..^&lt;/span&gt;&lt;span class="s"&gt;"_error"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicGetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errorQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoAck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BasicProperties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasicProperties&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasicProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"MT-Reason"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"MT-Redelivery-Count"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MT-Fault-"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// to the endpoint's exchange, body untouched: MassTransit dispatches on the envelope's messageType&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicPublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routingKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basicProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicAckAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only after the confirm&lt;/span&gt;
    &lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Messages you do not want to move (the two real bugs) need a filter before the publish: read &lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt; and leave those unacknowledged; they go back to the error queue when the channel closes. Add a delay between publishes for a large queue, and log what you moved where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Know your endpoints' &lt;code&gt;_error&lt;/code&gt; and &lt;code&gt;_skipped&lt;/code&gt; queues and watch their depth, or subscribe to &lt;code&gt;Fault&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;First-level retry for seconds, delayed redelivery for outages, so only real failures reach &lt;code&gt;_error&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Group the error queue by &lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt; before you move anything.&lt;/li&gt;
&lt;li&gt;Replay to the endpoint's exchange, body untouched, &lt;code&gt;MT-Fault-*&lt;/code&gt;, &lt;code&gt;MT-Reason&lt;/code&gt; and &lt;code&gt;MT-Redelivery-Count&lt;/code&gt; removed, confirmed before acknowledged.&lt;/li&gt;
&lt;li&gt;Skipped messages need their consumer first.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren recognises MassTransit's &lt;code&gt;_error&lt;/code&gt; and &lt;code&gt;_skipped&lt;/code&gt; queues as dead-letter queues, groups them by &lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt;, and says in one sentence which consumer faulted after how many retries. A replay goes to the endpoint's exchange with the fault and redelivery headers removed, the envelope untouched, confirmed message by message and throttled if you want, and every replay is in the audit log. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/replay.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>masstransit</category>
      <category>rabbitmq</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Your quorum dead-letter queue can drop the dead letters: x-delivery-count travels along</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:44:55 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/your-quorum-dead-letter-queue-can-drop-the-dead-letters-x-delivery-count-travels-along-240n</link>
      <guid>https://dev.to/viktorbaumann/your-quorum-dead-letter-queue-can-drop-the-dead-letters-x-delivery-count-travels-along-240n</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/rabbitmq-quorum-dlq-delivery-limit.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A dead-letter queue is supposed to be the place where failed messages are safe until someone looks at them. On RabbitMQ 4 with quorum queues and default settings, that is not quite true: a message that died because it was delivered too often can arrive in the DLQ with its delivery budget already used up, and the first consumer or tool that crashes while holding it, or even just peeks at it, makes RabbitMQ drop it for good. Here is what happens, measured on RabbitMQ 4.3.6, and three ways to make the DLQ safe again.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The short version as a video (1:42, captions, no sound needed):&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/QrpFgVE_I7I" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery limits in 30 seconds
&lt;/h2&gt;

&lt;p&gt;Quorum queues count how often a message was delivered and not settled. Past the queue's delivery limit (&lt;code&gt;x-delivery-limit&lt;/code&gt;, or &lt;code&gt;delivery-limit&lt;/code&gt; in a policy) RabbitMQ stops redelivering it: the message is dead-lettered with reason &lt;code&gt;delivery_limit&lt;/code&gt; if the queue has a dead-letter exchange, and dropped otherwise. Since RabbitMQ 4.0 every quorum queue has a limit of 20 unless you set one, which is good: a poison message no longer loops forever.&lt;/p&gt;

&lt;p&gt;What counts changed in 4.0 as well:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What happens to a delivered message&lt;/th&gt;
&lt;th&gt;3.13&lt;/th&gt;
&lt;th&gt;4.x&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;basic.nack&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;counts&lt;/td&gt;
&lt;td&gt;does not count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;basic.reject&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;counts&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;counts&lt;/strong&gt;, and the message goes to the back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;management UI "Get messages" in its default mode, "Nack message requeue true"&lt;/td&gt;
&lt;td&gt;counts&lt;/td&gt;
&lt;td&gt;does not count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the channel or connection closes while it is unacknowledged (consumer crash, pod killed, network drop)&lt;/td&gt;
&lt;td&gt;counts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;counts&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;basic.reject&lt;/code&gt; with &lt;code&gt;requeue=false&lt;/code&gt; (dead-lettered)&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;counts&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;put back (any of the above, or a peek with requeue) while its count is already &lt;strong&gt;above&lt;/strong&gt; the queue's limit&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;dropped&lt;/strong&gt;, or dead-lettered on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;put back (any of the above, or a peek with requeue) while its count is already &lt;strong&gt;above&lt;/strong&gt; the queue's limit&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;dropped&lt;/strong&gt;, or dead-lettered on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The count is visible on the message as the header &lt;code&gt;x-delivery-count&lt;/code&gt; (RabbitMQ 4 adds &lt;code&gt;x-acquired-count&lt;/code&gt;, every acquisition including requeues).&lt;/p&gt;

&lt;h2&gt;
  
  
  What we measured
&lt;/h2&gt;

&lt;p&gt;A work queue and a dead-letter queue, both quorum queues, no limits set, so both have RabbitMQ 4's default of 20:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.dlx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fanout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.dlq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;x-queue-type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;quorum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.dlq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.dlx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queue_declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.work&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;x-queue-type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;quorum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;x-dead-letter-exchange&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.dlx&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.work&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;order-4711&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a consumer that crashes on this message every time: fetch it without acknowledging and close the connection, until the work queue gives up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;message_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.work&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BlockingConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lim.work&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# the consumer "crashed" while holding the message
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;abandoned deliveries on lim.work until it gave up: 21
in lim.dlq: reason delivery_limit, x-delivery-count 21
left in lim.dlq after ONE abandoned delivery there: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message reached the DLQ, as it should, with reason &lt;code&gt;delivery_limit&lt;/code&gt;. But it brought its count of 21 along, and the DLQ's own limit is 20. The next abnormal return, a single one, made the DLQ give up on it too, and since the DLQ has no dead-letter exchange of its own, the message was dropped.&lt;/p&gt;

&lt;p&gt;It does not even take a crash. In a second run we only looked: one &lt;code&gt;basic.get&lt;/code&gt; followed by &lt;code&gt;basic.nack&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt;, and once more through the management UI's "Get messages" with requeue. Both times the DLQ was empty afterwards. On RabbitMQ 4.3.6 a message whose count is already above the queue's limit is dropped the moment it is put back, abnormally or not; a count equal to the limit survives. So the usual first step of an investigation, peeking at the DLQ, is what loses the message.&lt;/p&gt;

&lt;p&gt;It does not even take a crash. In a second run we only looked: one &lt;code&gt;basic.get&lt;/code&gt; followed by &lt;code&gt;basic.nack&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt;, and once more through the management UI's "Get messages" with requeue. Both times the DLQ was empty afterwards. On RabbitMQ 4.3.6 a message whose count is already above the queue's limit is dropped the moment it is put back, abnormally or not; a count equal to the limit survives. So the usual first step of an investigation, peeking at the DLQ, is what loses the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the count travels
&lt;/h2&gt;

&lt;p&gt;The delivery count is not just a header the queue writes for you to read: it is part of the message's annotations, and dead-lettering keeps them. The DLQ continues counting from where the work queue stopped. A header that a publisher sets is a different thing: we published a fresh message with &lt;code&gt;x-delivery-count: 5&lt;/code&gt; into a queue with a limit of 5, and RabbitMQ ignored it. The next delivery showed 1, and the message survived five abandoned deliveries. So the carried count comes from dead-lettering, not from the header itself.&lt;/p&gt;

&lt;p&gt;It is not only &lt;code&gt;delivery_limit&lt;/code&gt; messages. In the same test with explicit limits, a message that had been abandoned twice and then rejected arrived in the DLQ with a count of 3, and with a DLQ limit of 5 it was dropped after three more failures, not five. Every message rejected by a consumer arrives with at least 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who abandons a delivery in a DLQ?
&lt;/h2&gt;

&lt;p&gt;More often than you would think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a consumer on the DLQ that processes dead letters (an alerting bridge, an archiver, a "retry later" service) and crashes or is redeployed while holding messages with prefetch;&lt;/li&gt;
&lt;li&gt;a script that reads the DLQ with &lt;code&gt;basic.get&lt;/code&gt; and dies, or is stopped with Ctrl+C, before it acknowledges or nacks;&lt;/li&gt;
&lt;li&gt;a replay tool whose connection drops in the middle of a run;&lt;/li&gt;
&lt;li&gt;a pod eviction or a network blip during any of the above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On 3.13 it was worse (every requeue counted, so even looking at messages in the management UI cost deliveries), but 4.x's default limit makes the carry-over bite with no configuration at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three fixes
&lt;/h2&gt;

&lt;p&gt;All three kept the message through three abandoned deliveries in the DLQ in our test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No delivery limit on the DLQ.&lt;/strong&gt; A DLQ is not consumed in a loop, so it does not need one. By policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl set_policy dlq-no-limit &lt;span class="s1"&gt;'\.dlq$'&lt;/span&gt; &lt;span class="s1"&gt;'{"delivery-limit": -1}'&lt;/span&gt; &lt;span class="nt"&gt;--apply-to&lt;/span&gt; quorum_queues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or with &lt;code&gt;x-delivery-limit: -1&lt;/code&gt; when you declare the queue; a negative value means unlimited. Only one policy applies to a queue: if one matches your DLQs already, add the key there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A much higher limit,&lt;/strong&gt; if you want a backstop anyway: &lt;code&gt;x-delivery-limit: 100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A classic queue as DLQ.&lt;/strong&gt; Classic queues have no delivery limit. You lose the replication of a quorum queue, which matters less for a queue that is mostly written and rarely read; decide by how much you care about dead letters surviving the loss of a node.&lt;/p&gt;

&lt;p&gt;And in whatever reads the DLQ: settle every message you fetch, &lt;code&gt;ack&lt;/code&gt; what you have handled and &lt;code&gt;nack&lt;/code&gt; with &lt;code&gt;requeue=true&lt;/code&gt; what you have not. On 4.x a &lt;code&gt;nack&lt;/code&gt; with requeue costs nothing (a &lt;code&gt;reject&lt;/code&gt; with requeue does count), except for a message that is already above the queue's limit: the requeue drops it as well. Fix the limit before anyone reads the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking your own queues
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not peek first. Check the queue: &lt;code&gt;rabbitmqctl list_queues name type arguments policy&lt;/code&gt; shows which DLQs are quorum queues and whether a limit is set. A quorum DLQ with a limit can lose messages to the peek itself; set &lt;code&gt;delivery-limit: -1&lt;/code&gt; first, then look.&lt;/li&gt;
&lt;li&gt;After that, &lt;code&gt;x-delivery-count&lt;/code&gt; next to &lt;code&gt;x-death&lt;/code&gt; on a message tells you how much of its budget it used before it died.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;x-delivery-limit&lt;/code&gt; and no &lt;code&gt;delivery-limit&lt;/code&gt; policy means 20 on RabbitMQ 4.&lt;/li&gt;
&lt;li&gt;Replaying the message, by publishing it anew to its original exchange (a shovel does that too), gives it a fresh count. Dead-lettering it once more into another quorum queue does not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Quorum DLQs: &lt;code&gt;delivery-limit: -1&lt;/code&gt; by policy, or classic DLQs.&lt;/li&gt;
&lt;li&gt;Consumers and scripts on DLQs settle every message they fetch, also on shutdown.&lt;/li&gt;
&lt;li&gt;Take the limit off quorum DLQs before anyone peeks at them: on 4.x a peek drops what arrived above the limit.&lt;/li&gt;
&lt;li&gt;Replay to reset the budget; dead-lettering a message on into another quorum queue keeps the count.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren shows each message's remaining budget in a quorum DLQ ("1 of 20 left") and warns on the queue page when dead letters are one or two failed deliveries away from being dropped, saying whether RabbitMQ would drop them or dead-letter them again. Its replays publish the messages anew with a fresh count and strip the stale &lt;code&gt;x-delivery-count&lt;/code&gt;, and it reads nothing from a quorum queue where that can cost messages until you confirm: on 3.13 any quorum queue with a limit, on 4.x a quorum dead-letter queue with a limit, whose over-limit arrivals a peek would drop. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/quorum-queues.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>messaging</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Dead letters in Spring Boot: why RabbitMQ messages end up in the DLQ, and how to replay them</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:10:01 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/dead-letters-in-spring-boot-why-rabbitmq-messages-end-up-in-the-dlq-and-how-to-replay-them-5321</link>
      <guid>https://dev.to/viktorbaumann/dead-letters-in-spring-boot-why-rabbitmq-messages-end-up-in-the-dlq-and-how-to-replay-them-5321</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/spring-boot-rabbitmq-dead-letters.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;@RabbitListener&lt;/code&gt; throws. What happens next depends on three Spring AMQP settings most services never set on purpose: whether the message is requeued, whether it is retried, and what the last retry does with it. Get them wrong and one bad message loops forever or silently disappears. Get them right and it waits in a dead-letter queue until you have fixed the bug. Here is the whole chain, and how to put the messages back afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spring does when a listener throws
&lt;/h2&gt;

&lt;p&gt;By default the listener container catches the exception and rejects the message &lt;em&gt;with requeue&lt;/em&gt;. RabbitMQ puts it back at the head of the queue, the same consumer gets it again a millisecond later, throws again, and so on. A single poison message keeps a consumer busy, fills the log, and holds back everything behind it.&lt;/p&gt;

&lt;p&gt;Two exceptions to that: a &lt;code&gt;MessageConversionException&lt;/code&gt; (the payload could not be turned into your type) is treated as fatal by the default error handler and rejected without requeue. And if your code throws &lt;code&gt;AmqpRejectAndDontRequeueException&lt;/code&gt;, the message is rejected without requeue too. For everything else, switch the default off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rabbitmq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;listener&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;simple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;default-requeue-rejected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a failed message is rejected with &lt;code&gt;requeue=false&lt;/code&gt;. Whether that means "gone" or "dead-lettered" depends on the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the queue a dead-letter route
&lt;/h2&gt;

&lt;p&gt;A rejected message only reaches a dead-letter queue if its queue has a dead-letter exchange. In Spring you declare it with the queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;Queue&lt;/span&gt; &lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;QueueBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.process"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deadLetterExchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.dlx"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deadLetterRoutingKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.dlq"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;DirectExchange&lt;/span&gt; &lt;span class="nf"&gt;deadLetterExchange&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DirectExchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.dlx"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;Queue&lt;/span&gt; &lt;span class="nf"&gt;deadLetterQueue&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;QueueBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.dlq"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;Binding&lt;/span&gt; &lt;span class="nf"&gt;deadLetterBinding&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BindingBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadLetterQueue&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadLetterExchange&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.dlq"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RabbitMQ then republishes every rejected, expired or overflowing message from &lt;code&gt;orders.process&lt;/code&gt; to &lt;code&gt;orders.dlx&lt;/code&gt; with routing key &lt;code&gt;orders.dlq&lt;/code&gt;, and adds an &lt;code&gt;x-death&lt;/code&gt; header that says which queue it came from, why, and its original exchange and routing key. (&lt;a href="https://warrenops.io/blog/reading-the-x-death-header.html" rel="noopener noreferrer"&gt;Reading x-death&lt;/a&gt; explains every field.)&lt;/p&gt;

&lt;p&gt;One trap: queue arguments are fixed when a queue is created. If &lt;code&gt;orders.process&lt;/code&gt; already exists without them, Spring's declaration fails with &lt;code&gt;PRECONDITION_FAILED&lt;/code&gt; and the listener does not start. For existing queues use a policy instead, which can be added and changed at any time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl set_policy orders-dlx &lt;span class="s2"&gt;"^orders&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;process$"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"dead-letter-exchange":"orders.dlx","dead-letter-routing-key":"orders.dlq"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--apply-to&lt;/span&gt; queues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retry before giving up
&lt;/h2&gt;

&lt;p&gt;Many failures are transient: a timeout, a lock, a service that restarts. Spring Boot retries inside the listener before it rejects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rabbitmq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;listener&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;simple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;default-requeue-rejected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;max-attempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
          &lt;span class="na"&gt;initial-interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
          &lt;span class="na"&gt;multiplier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
          &lt;span class="na"&gt;max-interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know about this retry. It is &lt;em&gt;stateless&lt;/em&gt; and in memory: the consumer thread sleeps between attempts while the message stays unacknowledged, and with a prefetch of 250 the other 249 wait too. Keep the total backoff short, seconds rather than minutes. And because the attempts never left the consumer, RabbitMQ does not see them: the message arrives in the DLQ with &lt;code&gt;x-death&lt;/code&gt; count 1, although it failed four times.&lt;/p&gt;

&lt;p&gt;For longer delays (a downstream system that is down for ten minutes), let the broker wait instead: a wait queue with a TTL whose dead-letter exchange points back at the work exchange. Then every cycle shows up in &lt;code&gt;x-death&lt;/code&gt;, and the consumer can give up after a number of cycles by reading it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SuppressWarnings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unchecked"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;deathsIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;)&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessageProperties&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getHeaders&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-death"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deaths&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;deaths&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;death&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"queue"&lt;/span&gt;&lt;span class="o"&gt;))))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mapToLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;death&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"count"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;longValue&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the last attempt does: reject, or republish
&lt;/h2&gt;

&lt;p&gt;When the retries are used up, a &lt;code&gt;MessageRecoverer&lt;/code&gt; decides. The default rejects the message without requeue, so it is dead-lettered by the broker with &lt;code&gt;x-death&lt;/code&gt; as described above. The alternative is &lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;MessageRecoverer&lt;/span&gt; &lt;span class="nf"&gt;messageRecoverer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RabbitTemplate&lt;/span&gt; &lt;span class="n"&gt;rabbitTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// failed messages go to orders.error with the exception in the headers&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RepublishMessageRecoverer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rabbitTemplate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"orders.error"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"orders.process.failed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It publishes a copy to an exchange of your choice and acknowledges the original. The copy carries &lt;code&gt;x-exception-message&lt;/code&gt;, &lt;code&gt;x-exception-stacktrace&lt;/code&gt; (trimmed to fit the frame size), &lt;code&gt;x-original-exchange&lt;/code&gt; and &lt;code&gt;x-original-routingKey&lt;/code&gt;. The broker never dead-lettered it, so there is no &lt;code&gt;x-death&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Reject (default)&lt;/th&gt;
&lt;th&gt;RepublishMessageRecoverer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it lands&lt;/td&gt;
&lt;td&gt;The queue's dead-letter exchange&lt;/td&gt;
&lt;td&gt;The exchange you name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why it failed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-death&lt;/code&gt; reason &lt;code&gt;rejected&lt;/code&gt;, no exception&lt;/td&gt;
&lt;td&gt;The exception message and stack trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it came from&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-death&lt;/code&gt;: queue, exchange, routing keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-original-exchange&lt;/code&gt;, &lt;code&gt;x-original-routingKey&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs topology&lt;/td&gt;
&lt;td&gt;Dead-letter exchange on the queue&lt;/td&gt;
&lt;td&gt;Error exchange and queue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want to know &lt;em&gt;why&lt;/em&gt; a message failed without searching the logs, republish. If you want the broker to stay in charge (TTL, length limits and quorum delivery limits dead-letter the same way), reject. Many teams do both: rejected messages of a TTL wait queue, republished ones from the listener.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the messages back
&lt;/h2&gt;

&lt;p&gt;The bug is fixed, 300 messages wait in &lt;code&gt;orders.dlq&lt;/code&gt;. A replay has to get four things right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Acknowledge only after the broker confirmed the copy.&lt;/strong&gt; Publish with confirms, wait, then ack the dead letter. Ack first and a broken connection loses the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send it where it came from,&lt;/strong&gt; not to one queue for all: the exchange and routing key from &lt;code&gt;x-death&lt;/code&gt;, or the &lt;code&gt;x-original-*&lt;/code&gt; headers after a republish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip the bookkeeping.&lt;/strong&gt; A consumer that gives up after three deaths rejects a replayed message on arrival if its old &lt;code&gt;x-death&lt;/code&gt; is still there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go slowly&lt;/strong&gt; if the consumer just recovered. Three hundred messages in a second can be the second incident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With the client factory underneath Spring's and the plain channel API it looks like this; &lt;code&gt;originalRoute&lt;/code&gt; reads the first &lt;code&gt;x-death&lt;/code&gt; entry's exchange and routing key, or the &lt;code&gt;x-original-*&lt;/code&gt; pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** Moves up to [limit] messages from the DLQ back to where they came from. */&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;replay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CachingConnectionFactory&lt;/span&gt; &lt;span class="n"&gt;connectionFactory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dlq&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// a connection of its own from the underlying client factory: Spring's factory caches channels,&lt;/span&gt;
    &lt;span class="c1"&gt;// and confirm mode must not leak into that cache&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connectionFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRabbitConnectionFactory&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;newConnection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dlq-replay"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
         &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createChannel&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;confirmSelect&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;GetResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;basicGet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dlq&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the queue is empty&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProps&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeaders&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeaders&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;Route&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;originalRoute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// from x-death, or x-original-* after a republish&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keySet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;removeIf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-death"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-first-death"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-last-death"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-exception"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
                    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-original"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;basicPublish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;routingKey&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBody&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForConfirmsOrDie&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// the broker has it now&lt;/span&gt;
            &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;basicAck&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEnvelope&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getDeliveryTag&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only then remove it here&lt;/span&gt;
            &lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;moved&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not in this sketch, and worth adding before you run it in production: a return listener for unroutable messages (&lt;code&gt;mandatory=true&lt;/code&gt; sends them back instead of dropping them), a rate limit, selecting messages by exception rather than taking the first N, and a record of what was moved where. Do not use &lt;code&gt;RabbitTemplate.receive()&lt;/code&gt; for this: it acknowledges on receipt, before anything has been published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;default-requeue-rejected: false&lt;/code&gt;, so a failure is rejected instead of looping.&lt;/li&gt;
&lt;li&gt;A dead-letter exchange on every work queue, by argument for new queues, by policy for existing ones.&lt;/li&gt;
&lt;li&gt;Short in-memory retries; long delays through a TTL wait queue that shows up in &lt;code&gt;x-death&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RepublishMessageRecoverer&lt;/code&gt; if you want the exception next to the message.&lt;/li&gt;
&lt;li&gt;Replays that confirm before they ack, route back to the original exchange, and strip the death headers.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren reads both conventions: x-death from broker dead-lettering and the x-exception-* and x-original-* headers of Spring's RepublishMessageRecoverer. It groups a dead-letter queue by exception, shows each message's story, and replays the ones you select to their original exchange and routing key, death and exception headers stripped, confirmed message by message and throttled if you want. Every replay is in the audit log. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/replay.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; · &lt;a href="https://youtu.be/Hvbr_w_4GhA" rel="noopener noreferrer"&gt;4-minute walkthrough on YouTube&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>springboot</category>
      <category>java</category>
      <category>devops</category>
    </item>
    <item>
      <title>Alerting when a RabbitMQ queue has no consumers</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Fri, 02 Oct 2026 06:01:48 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/alerting-when-a-rabbitmq-queue-has-no-consumers-3joi</link>
      <guid>https://dev.to/viktorbaumann/alerting-when-a-rabbitmq-queue-has-no-consumers-3joi</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/alert-rabbitmq-queue-no-consumers.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A queue with messages and zero consumers is the quietest outage there is. Nothing errors. Producers publish happily. The dashboard shows the broker green. Then the TTL runs out, or the queue hits its limit, and an hour of orders is in a dead-letter queue. Here is how to get paged before that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to alert on
&lt;/h2&gt;

&lt;p&gt;"No consumers" alone is not the alert. Plenty of queues have no consumers by design: retry-wait queues, parking-lot DLQs, queues of a batch job that runs at night. The signal is the combination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;consumers == 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;messages_ready &amp;gt; 0&lt;/code&gt; (something is waiting)&lt;/li&gt;
&lt;li&gt;and it has been like that for a few minutes (a deploy restarts consumers; do not page on a 20-second gap)&lt;/li&gt;
&lt;li&gt;and the queue is not on your list of intentionally consumer-less queues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two neighbours of this alert are worth setting up at the same time, because they catch the cases "consumers exist but do nothing":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stuck consumers:&lt;/strong&gt; &lt;code&gt;consumers &amp;gt; 0&lt;/code&gt;, &lt;code&gt;messages_ready &amp;gt; 0&lt;/code&gt;, and the ack rate is zero for five minutes. The consumer process is alive, holds the connection, and is deadlocked or stuck on a downstream call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth:&lt;/strong&gt; &lt;code&gt;messages_ready&lt;/code&gt; grew by more than N in the last M minutes. Consumers are there but cannot keep up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the numbers come from
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Management HTTP API
&lt;/h3&gt;

&lt;p&gt;The management plugin exposes everything you need per queue. Ask for only the columns you want, otherwise the response for a broker with a thousand queues is huge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; monitor:secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'http://rabbit:15672/api/queues?columns=vhost,name,consumers,messages_ready,messages_unacknowledged,message_stats.ack_details.rate'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relevant fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;consumers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of consumers currently attached to the queue.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messages_ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Messages waiting to be delivered. This is the "backlog".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messages_unacknowledged&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delivered but not yet acked. High and flat means consumers hold messages without finishing them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message_stats.ack_details.rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Acks per second, averaged over the last sampling interval. Zero with a backlog and consumers present is the stuck-consumer signal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;consumer_utilisation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fraction of time consumers could take new messages. Low with a backlog means consumers are the bottleneck.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idle_since&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set when the queue had no activity at all since that time. Useful to ignore truly dead queues.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The monitoring user needs the &lt;code&gt;monitoring&lt;/code&gt; tag, nothing more. Do not use an administrator account for a poller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prometheus plugin
&lt;/h3&gt;

&lt;p&gt;Since RabbitMQ 3.8, &lt;code&gt;rabbitmq_prometheus&lt;/code&gt; ships with the broker. By default &lt;code&gt;/metrics&lt;/code&gt; is &lt;em&gt;aggregated&lt;/em&gt;: you get totals per node, not per queue, which is useless for this alert. Per-queue values come from either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/metrics/per-object&lt;/code&gt;, every metric for every object. Fine for a few hundred queues, heavy for thousands.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/metrics/detailed?family=queue_coarse_metrics&amp;amp;family=queue_consumer_count&lt;/code&gt; (3.9+), only the families you ask for. Metrics from this endpoint are prefixed &lt;code&gt;rabbitmq_detailed_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Or set &lt;code&gt;prometheus.return_per_object_metrics = true&lt;/code&gt; in &lt;code&gt;rabbitmq.conf&lt;/code&gt; to make &lt;code&gt;/metrics&lt;/code&gt; per-object.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmq-plugins &lt;span class="nb"&gt;enable &lt;/span&gt;rabbitmq_prometheus
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'http://rabbit:15692/metrics/detailed?family=queue_coarse_metrics&amp;amp;family=queue_consumer_count'&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prometheus alert rules
&lt;/h2&gt;

&lt;p&gt;With the detailed endpoint scraped as its own job, three rules cover the three signals above. Adjust the prefix (drop &lt;code&gt;detailed_&lt;/code&gt;) if you use per-object metrics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rabbitmq-queues&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RabbitMQQueueNoConsumers&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;rabbitmq_detailed_queue_consumers == 0&lt;/span&gt;
      &lt;span class="s"&gt;and on (vhost, queue) rabbitmq_detailed_queue_messages_ready{queue!~".*(\\.wait|\\.dlq|\\.parking)$"} &amp;gt; 0&lt;/span&gt;
    &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;page&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.vhost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.queue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;consumer"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RabbitMQQueueConsumersStuck&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;rabbitmq_detailed_queue_consumers &amp;gt; 0&lt;/span&gt;
      &lt;span class="s"&gt;and on (vhost, queue) rabbitmq_detailed_queue_messages_ready &amp;gt; 0&lt;/span&gt;
      &lt;span class="s"&gt;and on (vhost, queue) rate(rabbitmq_detailed_queue_messages_acked_total[5m]) == 0&lt;/span&gt;
    &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;page&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RabbitMQQueueGrowing&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;delta(rabbitmq_detailed_queue_messages_ready[15m]) &amp;gt; 1000&lt;/span&gt;
    &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0m&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;warn&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex on &lt;code&gt;queue&lt;/code&gt; excludes the queues that have no consumers on purpose; more on that below. Because the exclusion sits on the &lt;code&gt;messages_ready&lt;/code&gt; side of the &lt;code&gt;and&lt;/code&gt;, those queues never produce a result, whatever their consumer count.&lt;/p&gt;

&lt;p&gt;Route &lt;code&gt;severity: page&lt;/code&gt; to your on-call and &lt;code&gt;warn&lt;/code&gt; to a channel. A queue growing at 2 a.m. may be a batch job; a queue with no consumer at 2 a.m. is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Prometheus? Cron and curl
&lt;/h2&gt;

&lt;p&gt;A twenty-line script on a box that can reach the management API does the job for a small setup. This one posts to a Slack incoming webhook when a queue has a backlog and no consumer, and keeps a marker file so it does not repeat itself every minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://rabbit:15672&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;AUTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;monitor:secret
&lt;span class="nv"&gt;HOOK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://hooks.slack.com/services/T000/B000/xxxx
&lt;span class="nv"&gt;IGNORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\.(wait|dlq|parking)$'&lt;/span&gt;
&lt;span class="nv"&gt;STATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/tmp/rabbit-noconsumer&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# "vhost/queue&amp;lt;TAB&amp;gt;ready" for every queue that is in trouble right now&lt;/span&gt;
&lt;span class="nv"&gt;ALERTING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/api/queues?columns=vhost,name,consumers,messages_ready"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; ign &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IGNORE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="s1"&gt;'.[] | select(.consumers == 0 and .messages_ready &amp;gt; 0 and (.name | test($ign) | not))
           | "\(.vhost)/\(.name)\t\(.messages_ready)"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# notify once per incident: a marker file per queue&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; q ready&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;md5sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-32&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOOK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; t &lt;span class="s2"&gt;":rotating_light: &lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="s2"&gt; has &lt;/span&gt;&lt;span class="nv"&gt;$ready&lt;/span&gt;&lt;span class="s2"&gt; messages and no consumer"&lt;/span&gt; &lt;span class="s1"&gt;'{text: $t}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALERTING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# recovered: drop markers for queues that are no longer alerting&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qF&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ALERTING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it every minute from cron. The "for 5 minutes" part is missing here; add it by requiring the marker to be older than five minutes before posting, or accept the occasional deploy-time notification. Teams users swap the Slack payload for an Adaptive Card or a plain &lt;code&gt;{"text": ...}&lt;/code&gt; to an incoming webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding which queues to exclude
&lt;/h2&gt;

&lt;p&gt;Do this once and write it down, otherwise the alert gets muted the first week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry-wait queues&lt;/strong&gt; (TTL, dead-letter back to the work queue): never have consumers. Exclude by name pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead-letter and parking-lot queues&lt;/strong&gt;: no consumers, and messages in them are a &lt;em&gt;different&lt;/em&gt; alert (see below). Exclude here, alert separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch queues&lt;/strong&gt; consumed on a schedule: exclude, or alert only outside the schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else&lt;/strong&gt; with a backlog and no consumer is an incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Name your queues so a regex can tell these apart. &lt;code&gt;orders.process&lt;/code&gt;, &lt;code&gt;orders.retry.wait&lt;/code&gt;, &lt;code&gt;orders.dlq&lt;/code&gt; is a convention that makes every rule on this page a one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The companion alert: something landed in a DLQ
&lt;/h2&gt;

&lt;p&gt;The no-consumer alert is the early warning. The late warning is &lt;code&gt;messages_ready &amp;gt; 0&lt;/code&gt; on a queue matching &lt;code&gt;\.dlq$&lt;/code&gt;, because by then messages have already failed. Give that one a lower threshold than you think: one dead letter in a queue that is normally empty is news, three hundred is a postmortem. If you want fewer false alarms, alert on the rate of arrivals into the DLQ (&lt;code&gt;message_stats.publish_details.rate&lt;/code&gt; on the DLQ, or &lt;code&gt;rate(rabbitmq_detailed_queue_messages_published_total[5m])&lt;/code&gt;) rather than on the count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the history
&lt;/h2&gt;

&lt;p&gt;Whatever alerts you build, keep the per-queue time series for at least a week. The question after a no-consumer page is always "since when", and the management UI only shows the last hour at any useful resolution. Prometheus gives you this for free. The cron script does not; if you go that route, at least append the numbers to a file per run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;If you do not run Prometheus, Warren's Team edition does this out of the box: alert rules match queues by regex on one or all clusters, with conditions for messages above a threshold, no consumers while messages wait, and growth within a window, each with a sustained-for time. Notifications go to Slack, Teams or a JSON webhook, repeat after a configurable interval and resolve when the queue recovers. Every queue is sampled every 30 seconds and kept for seven days, so "since when" has an answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/alert-rules.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; · &lt;a href="https://youtu.be/Hvbr_w_4GhA" rel="noopener noreferrer"&gt;4-minute walkthrough on YouTube&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>prometheus</category>
    </item>
    <item>
      <title>Reading the x-death header: which queue rejected the message, and why</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Thu, 01 Oct 2026 06:28:52 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/reading-the-x-death-header-which-queue-rejected-the-message-and-why-4oea</link>
      <guid>https://dev.to/viktorbaumann/reading-the-x-death-header-which-queue-rejected-the-message-and-why-4oea</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/reading-the-x-death-header.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every message in a dead-letter queue carries its own incident report. RabbitMQ writes it into the &lt;code&gt;x-death&lt;/code&gt; header when it dead-letters the message. Most people have seen it in the management UI as a wall of nested tables and closed the tab. Here is how to read it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related, in a 1:13 video (captions, no sound needed): why the x-death count starts over after a replay.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8cxcRIMEgeA" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it comes from
&lt;/h2&gt;

&lt;p&gt;When RabbitMQ dead-letters a message, it does not just move it. It republishes the message to the queue's dead-letter exchange (set by the &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; queue argument or a &lt;code&gt;dead-letter-exchange&lt;/code&gt; policy), optionally with a new routing key (&lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;). Before publishing, it modifies the headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It adds or updates an &lt;code&gt;x-death&lt;/code&gt; header, an array of tables.&lt;/li&gt;
&lt;li&gt;The first time a message is dead-lettered it also sets &lt;code&gt;x-first-death-reason&lt;/code&gt;, &lt;code&gt;x-first-death-queue&lt;/code&gt; and &lt;code&gt;x-first-death-exchange&lt;/code&gt;. These are never changed afterwards.&lt;/li&gt;
&lt;li&gt;Recent RabbitMQ versions also set &lt;code&gt;x-last-death-reason&lt;/code&gt;, &lt;code&gt;x-last-death-queue&lt;/code&gt; and &lt;code&gt;x-last-death-exchange&lt;/code&gt;, updated on every dead-lettering.&lt;/li&gt;
&lt;li&gt;If the message died because it &lt;em&gt;expired&lt;/em&gt; and had a per-message TTL, the &lt;code&gt;expiration&lt;/code&gt; property is removed (otherwise it would expire again in the DLQ) and preserved in the x-death entry as &lt;code&gt;original-expiration&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else, payload, properties, your own headers, is untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entry
&lt;/h2&gt;

&lt;p&gt;A typical header, as the management UI or a client shows it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;x-death:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rejected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"queue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1759042215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"routing-keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"order.created"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"queue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.retry.wait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1759042155&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.retry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"routing-keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"order.created"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;x-first-death-exchange:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;orders&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;x-first-death-queue:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;orders.process&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;x-first-death-reason:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;rejected&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The queue the message was in when it was dead-lettered. This is the queue whose consumer rejected it, whose TTL ran out, or whose length limit hit. Not the queue it landed in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reason&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rejected&lt;/code&gt;: a consumer did &lt;code&gt;basic.reject&lt;/code&gt; or &lt;code&gt;basic.nack&lt;/code&gt; with &lt;code&gt;requeue=false&lt;/code&gt;. &lt;code&gt;expired&lt;/code&gt;: message or queue TTL ran out. &lt;code&gt;maxlen&lt;/code&gt;: the queue was over &lt;code&gt;x-max-length&lt;/code&gt; or &lt;code&gt;x-max-length-bytes&lt;/code&gt; and the overflow policy is &lt;code&gt;drop-head&lt;/code&gt; or &lt;code&gt;reject-publish-dlx&lt;/code&gt;. &lt;code&gt;delivery_limit&lt;/code&gt;: a quorum queue redelivered the message more often than its &lt;code&gt;delivery-limit&lt;/code&gt; allows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unix timestamp (seconds) of the &lt;em&gt;first&lt;/em&gt; time this queue dead-lettered the message for this reason. When &lt;code&gt;count&lt;/code&gt; grows, &lt;code&gt;time&lt;/code&gt; does not move.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The exchange the message was originally published to before it arrived in &lt;code&gt;queue&lt;/code&gt;. Together with &lt;code&gt;routing-keys&lt;/code&gt; this is where a replay sends it back.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;routing-keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The routing key(s) the message was published with. Usually one. Can be several if the message was CC'd or BCC'd.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many times this message was dead-lettered from this queue for this reason. Entries are not appended per event; RabbitMQ finds the entry with the same &lt;code&gt;queue&lt;/code&gt; and &lt;code&gt;reason&lt;/code&gt; and increments it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;original-expiration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only present when &lt;code&gt;reason&lt;/code&gt; is &lt;code&gt;expired&lt;/code&gt; and the message had a per-message &lt;code&gt;expiration&lt;/code&gt;. The value that was removed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Reading the array
&lt;/h2&gt;

&lt;p&gt;The array is ordered most recent first: &lt;code&gt;x-death[0]&lt;/code&gt; is the last thing that happened to the message. In the example above, the story reads:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The message was published to exchange &lt;code&gt;orders&lt;/code&gt; with key &lt;code&gt;order.created&lt;/code&gt; and landed in &lt;code&gt;orders.process&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The consumer rejected it. It went to &lt;code&gt;orders.retry.wait&lt;/code&gt;, a queue with a TTL and no consumers.&lt;/li&gt;
&lt;li&gt;It expired there and came back to &lt;code&gt;orders.process&lt;/code&gt;. Rejected again. Expired again. Rejected a third time.&lt;/li&gt;
&lt;li&gt;After the third rejection (&lt;code&gt;count: 3&lt;/code&gt;) the message ended up wherever you are looking at it now, presumably a final DLQ, because the consumer stopped requeueing after three attempts or the retry queue's TTL routed it elsewhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note what the header does &lt;em&gt;not&lt;/em&gt; tell you: which DLQ the message is in now (you know that, you are looking at it), and &lt;em&gt;what&lt;/em&gt; went wrong in the consumer. For that you need the consumer's log at &lt;code&gt;time&lt;/code&gt;, and &lt;code&gt;time&lt;/code&gt; is the first rejection, not the last. If your retry cycle is 60 seconds and &lt;code&gt;count&lt;/code&gt; is 3, the last rejection was about two minutes after &lt;code&gt;time&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same queue twice with different reasons
&lt;/h3&gt;

&lt;p&gt;Because entries are keyed by queue and reason, a queue can appear twice: once with &lt;code&gt;rejected&lt;/code&gt;, once with &lt;code&gt;expired&lt;/code&gt;. That usually means a consumer was down for a while (messages expired) and then came back and rejected the same message. Both entries carry their own &lt;code&gt;time&lt;/code&gt;, so you can tell when each phase started.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why time does not update
&lt;/h3&gt;

&lt;p&gt;People look at &lt;code&gt;time&lt;/code&gt;, see a timestamp from three days ago, and conclude the message has been sitting in the DLQ for three days. It may have arrived a minute ago after its tenth cycle. To know when a message actually entered the DLQ you need something outside the header: a &lt;code&gt;timestamp&lt;/code&gt; property your producer set (that is when it was &lt;em&gt;published&lt;/em&gt;, also wrong), a message-level TTL trick, or a tool that remembers when it first saw the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the header looks like for each cause
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;Typical pattern&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rejected&lt;/code&gt;, count 1&lt;/td&gt;
&lt;td&gt;Single entry, one queue&lt;/td&gt;
&lt;td&gt;Consumer log at &lt;code&gt;time&lt;/code&gt;. Parsing error or a business rule, usually deterministic. Fix, then replay.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rejected&lt;/code&gt;, count &amp;gt; 1&lt;/td&gt;
&lt;td&gt;Alternating with an &lt;code&gt;expired&lt;/code&gt; entry from a wait queue&lt;/td&gt;
&lt;td&gt;Retry topology did its job. Either a transient failure that outlasted the retries, or a real bug. The consumer log at the &lt;em&gt;last&lt;/em&gt; attempt tells which.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;expired&lt;/code&gt;, from a work queue&lt;/td&gt;
&lt;td&gt;Single entry, the exchange is the normal one&lt;/td&gt;
&lt;td&gt;Nobody consumed in time. Consumer count on that queue, consumer throughput, was the TTL sane.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxlen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Many messages with identical &lt;code&gt;time&lt;/code&gt;, seconds apart&lt;/td&gt;
&lt;td&gt;A burst the consumer could not absorb. Producer side, or scale consumers. These messages are usually fine to replay once the backlog is gone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delivery_limit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quorum queue, no &lt;code&gt;rejected&lt;/code&gt; entry&lt;/td&gt;
&lt;td&gt;The consumer crashed or its connection dropped repeatedly while holding the message, without ever rejecting it. Classic poison message or a consumer that dies on this payload.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Messages with no x-death at all
&lt;/h2&gt;

&lt;p&gt;Not every message in a dead-letter queue was dead-lettered by RabbitMQ. Many frameworks catch the exception and &lt;em&gt;republish&lt;/em&gt; the message to an error queue themselves. The broker never dead-letters it, so there is no &lt;code&gt;x-death&lt;/code&gt;. Instead you get framework headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spring AMQP&lt;/strong&gt; (&lt;code&gt;RepublishMessageRecoverer&lt;/code&gt;): &lt;code&gt;x-exception-message&lt;/code&gt;, &lt;code&gt;x-exception-stacktrace&lt;/code&gt;, &lt;code&gt;x-original-exchange&lt;/code&gt;, &lt;code&gt;x-original-routingKey&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MassTransit&lt;/strong&gt; (&lt;code&gt;_error&lt;/code&gt; queues): &lt;code&gt;MT-Fault-Message&lt;/code&gt;, &lt;code&gt;MT-Fault-ExceptionType&lt;/code&gt;, &lt;code&gt;MT-Fault-StackTrace&lt;/code&gt;, &lt;code&gt;MT-Reason&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NServiceBus&lt;/strong&gt;: &lt;code&gt;NServiceBus.ExceptionInfo.ExceptionType&lt;/code&gt;, &lt;code&gt;NServiceBus.ExceptionInfo.Message&lt;/code&gt;, &lt;code&gt;NServiceBus.FailedQ&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are often more useful than &lt;code&gt;x-death&lt;/code&gt; because they carry the actual exception. But the "where to send it back" question then has to be answered from &lt;code&gt;x-original-*&lt;/code&gt; or &lt;code&gt;NServiceBus.FailedQ&lt;/code&gt;, and there is no &lt;code&gt;count&lt;/code&gt;. Your replay tooling should read both conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to look at it
&lt;/h2&gt;

&lt;p&gt;For one message at a time, paste it into the &lt;a href="https://warrenops.io/tools/x-death-decoder.html" rel="noopener noreferrer"&gt;x-death decoder&lt;/a&gt;: it tells the story above in plain words, also for the Spring, MassTransit and NServiceBus headers, and runs in your browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Management UI
&lt;/h3&gt;

&lt;p&gt;Queue page → &lt;em&gt;Get messages&lt;/em&gt;, ack mode &lt;em&gt;Nack message requeue true&lt;/em&gt;, and a small count. The UI renders &lt;code&gt;x-death&lt;/code&gt; as nested tables. It works for one or two messages; for fifty it is unreadable, and the peek marks every fetched message as &lt;code&gt;redelivered&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqadmin &lt;span class="nt"&gt;-f&lt;/span&gt; raw_json get &lt;span class="nv"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;orders.dlq &lt;span class="nv"&gt;ackmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ack_requeue_true &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.[] | .properties.headers["x-death"]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-f raw_json&lt;/code&gt; makes &lt;code&gt;rabbitmqadmin&lt;/code&gt; print the messages as JSON instead of a table, and &lt;code&gt;jq&lt;/code&gt; pulls out the header. &lt;code&gt;ack_requeue_true&lt;/code&gt; puts the messages back; they are marked &lt;code&gt;redelivered&lt;/code&gt; afterwards, which is a property of RabbitMQ and cannot be avoided when peeking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;death_story&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routing-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;x &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;queue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; / &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, first at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Java client the values arrive as &lt;code&gt;List&amp;lt;Map&amp;lt;String,Object&amp;gt;&amp;gt;&lt;/code&gt; with &lt;code&gt;LongString&lt;/code&gt; values for strings; call &lt;code&gt;toString()&lt;/code&gt; on them before comparing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules for your own consumers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In a retry loop, read the &lt;code&gt;x-death&lt;/code&gt; count to decide when to give up.&lt;/strong&gt; When a message cycles through a wait queue, the broker counts every round in the entry for your queue; look for the entry whose &lt;code&gt;queue&lt;/code&gt; is your own queue, not blindly &lt;code&gt;[0]&lt;/code&gt;. The count only covers dead-lettering inside the broker: after a republish (a replay, or a consumer that publishes the message again) RabbitMQ 4 starts again at 1, even if the publisher sent the old header along. To count across republishes, use a header of your own; application headers survive dead-lettering unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip the death headers when you deliberately republish for a fresh start.&lt;/strong&gt; A replay tool should do the same. A message that keeps its &lt;code&gt;x-death&lt;/code&gt; after a replay will be rejected on arrival by any consumer that follows rule one.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren shows each dead letter's x-death as a readable timeline: which queue, why, how often, when, and a plain-language verdict such as "rejected three times by orders.process after cycling through orders.retry.wait". Messages a framework republished itself are recognised by their x-exception-* or MT-Fault-* headers, and "dead since" is filled from when Warren first saw the message, which x-death cannot tell you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/message-view.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; · &lt;a href="https://youtu.be/Hvbr_w_4GhA" rel="noopener noreferrer"&gt;4-minute walkthrough on YouTube&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>messaging</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How to replay dead-lettered messages in RabbitMQ</title>
      <dc:creator>Viktor</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:09:33 +0000</pubDate>
      <link>https://dev.to/viktorbaumann/how-to-replay-dead-lettered-messages-in-rabbitmq-3mel</link>
      <guid>https://dev.to/viktorbaumann/how-to-replay-dead-lettered-messages-in-rabbitmq-3mel</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://warrenops.io/blog/replay-dead-lettered-messages-rabbitmq.html" rel="noopener noreferrer"&gt;warrenops.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A dead-letter queue is filling up, the bug that caused it is fixed, and now 300 messages have to go back to where they came from. This is what your options are, what each one gets wrong, and the details that decide whether the replay works or produces a second incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually dealing with
&lt;/h2&gt;

&lt;p&gt;RabbitMQ dead-letters a message when a consumer rejects it with &lt;code&gt;requeue=false&lt;/code&gt;, when its TTL expires, when the queue is over its length limit, or (quorum queues) when the delivery limit is reached. The broker republishes the message to the queue's dead-letter exchange, and on the way it adds an &lt;code&gt;x-death&lt;/code&gt; header: which queue it came from, why, when, the original exchange and routing keys. (&lt;a href="https://warrenops.io/blog/reading-the-x-death-header.html" rel="noopener noreferrer"&gt;Reading x-death&lt;/a&gt; is its own article.)&lt;/p&gt;

&lt;p&gt;That header is the key to a correct replay. It tells you where the message was supposed to go. It is also the thing that trips up most home-made replays, because the header &lt;em&gt;stays&lt;/em&gt; on the message unless you remove it, and consumers with retry logic often read it.&lt;/p&gt;

&lt;p&gt;Three questions before you move anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the cause fixed?&lt;/strong&gt; Replaying into a consumer that still fails just cycles the messages back into the DLQ, with a bigger &lt;code&gt;x-death&lt;/code&gt; count each time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where do they go back to?&lt;/strong&gt; To the original exchange with the original routing key (so bindings decide again), or directly to the queue they died in? The first is correct if the topology has changed; the second is what most people mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All of them, or some?&lt;/strong&gt; A DLQ usually mixes several failure causes. Replaying everything replays the poison messages too.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Option 1: the management UI's "Move messages"
&lt;/h2&gt;

&lt;p&gt;With the &lt;code&gt;rabbitmq_shovel&lt;/code&gt; and &lt;code&gt;rabbitmq_shovel_management&lt;/code&gt; plugins enabled, every queue page in the management UI has a &lt;em&gt;Move messages&lt;/em&gt; panel: type a destination queue name, click, done. Under the hood it creates a temporary shovel that drains the queue into the destination via the default exchange.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmq-plugins &lt;span class="nb"&gt;enable &lt;/span&gt;rabbitmq_shovel rabbitmq_shovel_management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good for: one queue, all messages, right now, you trust every message in there.&lt;/p&gt;

&lt;p&gt;Not good for: anything selective. It moves everything, including the ten messages that are the actual poison. It does not strip &lt;code&gt;x-death&lt;/code&gt;, so consumers that count retries may reject them on arrival. It publishes with no confirms visible to you and leaves no record of who moved what. And it only targets a queue by name, never the original exchange.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: a shovel you define yourself
&lt;/h2&gt;

&lt;p&gt;A dynamic shovel gives you a bit more control than the button. &lt;code&gt;src-delete-after: queue-length&lt;/code&gt; makes it stop after moving the messages that were in the queue when it started, so new dead letters arriving during the replay are not dragged along.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl set_parameter shovel replay-orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"src-protocol": "amqp091", "src-uri": "amqp://", "src-queue": "orders.dlq",
    "dest-protocol": "amqp091", "dest-uri": "amqp://",
    "dest-exchange": "orders", "dest-exchange-key": "order.created",
    "src-delete-after": "queue-length", "ack-mode": "on-confirm"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ack-mode: on-confirm&lt;/code&gt; is the important line: the shovel acknowledges a source message only after the destination confirmed it, so a crash mid-way duplicates at most one message and loses none.&lt;/p&gt;

&lt;p&gt;Limits: one destination for the whole batch. If your DLQ collects messages from five different routing keys (common when one DLX serves a whole service), a shovel sends them all to the same place. And again: no selection, no header cleanup, no audit beyond the shovel's own log line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: a script
&lt;/h2&gt;

&lt;p&gt;This is what most teams end up with at 3 a.m., and it is fine if it gets four things right. In Python with &lt;code&gt;pika&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pika&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BlockingConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pika&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;URLParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amqp://user:pass@rabbit/%2F&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm_delivery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                     &lt;span class="c1"&gt;# 1. publisher confirms
&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_ack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 2. never auto-ack
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;death&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[{}])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;routing_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;death&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routing-keys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;routing_key&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;looks_replayable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;            &lt;span class="c1"&gt;# 3. select (your own check)
&lt;/span&gt;        &lt;span class="k"&gt;continue&lt;/span&gt;                                         &lt;span class="c1"&gt;# leave it unacked, see 3.
&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                             &lt;span class="c1"&gt;# 4. strip death bookkeeping
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-first-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-last-death&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;

    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routing_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# only after the confirm above returned
&lt;/span&gt;
&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basic_nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# puts every skipped message back
&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four things, in order of how often they go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirms before acks.&lt;/strong&gt; &lt;code&gt;confirm_delivery()&lt;/code&gt; turns &lt;code&gt;basic_publish&lt;/code&gt; into a call that raises when the broker did not accept the message. Ack the source message only after that. Ack first and a broker hiccup deletes messages you never republished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-ack.&lt;/strong&gt; With &lt;code&gt;auto_ack=True&lt;/code&gt; the message is gone the moment you fetched it. If the script dies between fetch and publish, so is the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select, do not drain.&lt;/strong&gt; Leave the messages you skip unacknowledged. While they are outstanding on the channel, the next &lt;code&gt;basic_get&lt;/code&gt; returns the next message, and one &lt;code&gt;basic_nack(0, multiple=True)&lt;/code&gt; at the end puts all of them back. Do not &lt;code&gt;basic_nack&lt;/code&gt; each one with &lt;code&gt;requeue=True&lt;/code&gt; right away: it goes back to the head of the queue and the next &lt;code&gt;basic_get&lt;/code&gt; returns the same message again, forever. And do not leave them to the connection close: on a quorum queue that counts as a delivery, and after 21 such scans the default delivery limit drops the message (measured on RabbitMQ 4.3.6; &lt;a href="https://warrenops.io/blog/replay-some-dead-letters-rabbitmq.html" rel="noopener noreferrer"&gt;details&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip the death headers.&lt;/strong&gt; Otherwise the consumer sees a message with &lt;code&gt;x-death[0].count = 3&lt;/code&gt; and, if it counts retries, gives up immediately. Also look for application-level retry headers such as &lt;code&gt;x-retry-count&lt;/code&gt; or Spring's &lt;code&gt;x-exception-*&lt;/code&gt;; a replay is supposed to be a fresh start.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;mandatory=True&lt;/code&gt; makes the broker return the message (&lt;code&gt;basic.return&lt;/code&gt;) instead of silently dropping it when the routing key no longer matches any binding, which happens more often than you would think after a deploy renamed a queue. Register a return callback or the confirm will look like a success.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flooding.&lt;/strong&gt; Three hundred messages arriving in the same second at a consumer that was just restarted is how you get a second outage. Sleep between publishes, or replay in batches of fifty and watch the consumer's error rate in between.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Same idea in Java
&lt;/h3&gt;

&lt;p&gt;With the RabbitMQ Java client: &lt;code&gt;channel.confirmSelect()&lt;/code&gt;, &lt;code&gt;channel.basicGet(queue, false)&lt;/code&gt;, publish with &lt;code&gt;channel.basicPublish(exchange, key, true, props, body)&lt;/code&gt;, then &lt;code&gt;channel.waitForConfirmsOrDie()&lt;/code&gt;, then &lt;code&gt;channel.basicAck(tag, false)&lt;/code&gt;. Spring AMQP users: &lt;code&gt;RabbitTemplate&lt;/code&gt; with &lt;code&gt;publisher-confirm-type: correlated&lt;/code&gt; and a &lt;code&gt;ConfirmCallback&lt;/code&gt;, and take the messages with &lt;code&gt;rabbitTemplate.receive(queue)&lt;/code&gt; inside a transaction or with manual acks via &lt;code&gt;execute(ChannelCallback)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 4: do not replay by hand at all
&lt;/h2&gt;

&lt;p&gt;Some failures are transient by nature: a downstream API was down for two minutes. For those, a retry topology handles the replay before anyone is paged: the consumer rejects into a &lt;em&gt;wait&lt;/em&gt; queue with a per-queue TTL and no consumers, whose dead-letter exchange points back at the work queue. After N cycles (read &lt;code&gt;x-death[0].count&lt;/code&gt;) the consumer routes the message to a final &lt;em&gt;parking lot&lt;/em&gt; DLQ that a human looks at.&lt;/p&gt;

&lt;p&gt;This does not remove the need for a manual replay; it removes the manual replays that were never necessary. The parking lot still fills up with the messages that failed for a real reason, and those you replay after the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Move button&lt;/th&gt;
&lt;th&gt;Shovel&lt;/th&gt;
&lt;th&gt;Script&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Select which messages&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original exchange and key per message&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no (one target)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strip x-death and retry headers&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;if you write it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loss-free (confirm before ack)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;with &lt;code&gt;on-confirm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;if you write it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throttle&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;if you write it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who moved what, when&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;if you log it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit a message before replay&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;if you write it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Whatever you use: replay a handful first, watch the consumer, then the rest. And write down what you did. The postmortem will ask.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Warren fits
&lt;/h2&gt;

&lt;p&gt;Warren is the script above as a tool: browse the DLQ, see the payload and why each message died, select some or the first N, replay them to their original exchange and routing key, throttled, with death and retry headers stripped and publisher confirms on every message. Every replay is recorded with who, when and the per-message outcome. Free for one broker.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The 4-minute walkthrough, from 200 dead letters to a throttled replay:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Hvbr_w_4GhA" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://warrenops.io/#deploy" rel="noopener noreferrer"&gt;Try Warren in a minute&lt;/a&gt; (one compose file, demo broker with real dead letters included) · &lt;a href="https://warrenops.io/docs/replay.html" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; · &lt;a href="https://youtu.be/Hvbr_w_4GhA" rel="noopener noreferrer"&gt;4-minute walkthrough on YouTube&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>devops</category>
      <category>messaging</category>
      <category>python</category>
    </item>
  </channel>
</rss>
