<?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: Emmanuel Igunnu</title>
    <description>The latest articles on DEV Community by Emmanuel Igunnu (@daemonicc).</description>
    <link>https://dev.to/daemonicc</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%2F236454%2F123327cd-49e6-454f-be28-12209ff2f804.png</url>
      <title>DEV Community: Emmanuel Igunnu</title>
      <link>https://dev.to/daemonicc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daemonicc"/>
    <language>en</language>
    <item>
      <title>Your RabbitMQ Consumer Reconnected. The In-Flight Handler Still Crashed.</title>
      <dc:creator>Emmanuel Igunnu</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:30:06 +0000</pubDate>
      <link>https://dev.to/daemonicc/your-rabbitmq-consumer-reconnected-the-in-flight-handler-still-crashed-2h76</link>
      <guid>https://dev.to/daemonicc/your-rabbitmq-consumer-reconnected-the-in-flight-handler-still-crashed-2h76</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/daemonicc/a-rabbitmq-upgrade-exposed-the-reliability-assumptions-hidden-in-our-messaging-system-3c77"&gt;previous article,&lt;/a&gt; I wrote about how a RabbitMQ upgrade exposed assumptions hidden inside our messaging system.&lt;/p&gt;

&lt;p&gt;One failure mode deserved a deeper explanation because it reveals an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovering a consumer does not necessarily recover the work that consumer was already processing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our reconnection logic could establish a new connection, create a new channel, redeclare the queue, reapply prefetch, and resume consumption. From the outside, the system looked healthy again. But an older message handler could still be running with a reference to the channel that had just closed. When that handler finished and attempted to acknowledge its message, it failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IllegalOperationError: Channel closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer had recovered. The in-flight handler had not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Failure Timeline
&lt;/h2&gt;

&lt;p&gt;Consider a consumer processing a transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process_transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&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;await&lt;/span&gt; &lt;span class="nf"&gt;processTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&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;Under stable conditions, the flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;RabbitMQ delivers a message.&lt;/li&gt;
&lt;li&gt;The handler processes the transfer.&lt;/li&gt;
&lt;li&gt;The consumer acknowledges the message.&lt;/li&gt;
&lt;li&gt;RabbitMQ removes it from the queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now introduce a broker restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RabbitMQ delivers message
        ↓
Handler begins processing
        ↓
RabbitMQ connection closes
        ↓
Consumer starts reconnecting
        ↓
A new connection and channel are created
        ↓
Old handler finishes processing
        ↓
Old handler calls channel.ack(msg)
        ↓
IllegalOperationError: Channel closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is that reconnecting does not replace the channel captured by the existing handler. That handler belongs to the lifetime of the old channel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recovery Has Two Timelines
&lt;/h2&gt;

&lt;p&gt;It helps to think of recovery as two separate timelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  The future timeline
&lt;/h3&gt;

&lt;p&gt;This is the part most reconnect implementations handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect the closed connection.&lt;/li&gt;
&lt;li&gt;Open a new connection.&lt;/li&gt;
&lt;li&gt;Create a new channel.&lt;/li&gt;
&lt;li&gt;Redeclare exchanges and queues.&lt;/li&gt;
&lt;li&gt;Reapply prefetch.&lt;/li&gt;
&lt;li&gt;Register the consumer again.&lt;/li&gt;
&lt;li&gt;Continue receiving messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This protects future work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The current timeline
&lt;/h3&gt;

&lt;p&gt;This contains work that was already running when the connection disappeared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database writes may still be executing.&lt;/li&gt;
&lt;li&gt;External API requests may still be in progress.&lt;/li&gt;
&lt;li&gt;A long-running handler may still complete.&lt;/li&gt;
&lt;li&gt;Business logic may succeed after its RabbitMQ channel has died.&lt;/li&gt;
&lt;li&gt;The final acknowledgement may target invalid infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reconnection does not automatically resolve any of these cases. A reliable consumer must handle both timelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Delivery Belongs to Its Original Channel
&lt;/h2&gt;

&lt;p&gt;An AMQP channel is not merely a convenient object through which we call &lt;code&gt;ack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is part of the identity of the delivery.&lt;/p&gt;

&lt;p&gt;RabbitMQ delivery tags are scoped to a channel. A message delivered on one channel must be acknowledged on that same channel. Attempting to acknowledge it through the replacement channel can cause an &lt;code&gt;unknown delivery tag&lt;/code&gt; channel error.&lt;/p&gt;

&lt;p&gt;This means the following approach is unsafe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Consumer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConsumeMessage&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="nf"&gt;processMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This may now point to a replacement channel.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;If &lt;code&gt;this.channel&lt;/code&gt; changes during reconnection, the handler may use the new channel to acknowledge a delivery received through the old one.&lt;/p&gt;

&lt;p&gt;The new channel is healthy, but it does not own that delivery.&lt;/p&gt;

&lt;p&gt;RabbitMQ’s &lt;a href="https://www.rabbitmq.com/docs/confirms" rel="noopener noreferrer"&gt;acknowledgement documentation&lt;/a&gt; explicitly requires deliveries to be acknowledged on the channel on which they were received.&lt;/p&gt;

&lt;p&gt;A reconnect implementation therefore needs to preserve the relationship between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The message&lt;/li&gt;
&lt;li&gt;The handler&lt;/li&gt;
&lt;li&gt;The channel that delivered the message&lt;/li&gt;
&lt;li&gt;The lifetime of that channel&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Acknowledgements Need Defensive Handling
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;amqplib&lt;/code&gt;, &lt;code&gt;ack&lt;/code&gt;, &lt;code&gt;nack&lt;/code&gt;, and &lt;code&gt;reject&lt;/code&gt; do not return promises. They send protocol commands without waiting for a response.&lt;/p&gt;

&lt;p&gt;This code does not provide useful protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing meaningful to await.&lt;/p&gt;

&lt;p&gt;If the library already knows that the channel is closed, the operation can throw synchronously. That means settlement operations should be guarded with &lt;code&gt;try/catch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a simplified pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ConsumeMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amqplib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safelySettle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&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="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;channelClosed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelClosed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Skipping message settlement on a closed channel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to settle RabbitMQ delivery&lt;/span&gt;&lt;span class="dl"&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;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;The consumer can capture the channel that delivered the message and track its state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;channelClosed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;channelClosed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RabbitMQ channel error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&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="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefetch&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process_transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConsumeMessage&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;;&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;await&lt;/span&gt; &lt;span class="nf"&gt;processTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;safelySettle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process_transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;channelClosed&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transfer processing failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nf"&gt;safelySettle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process_transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;channelClosed&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;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;noAck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;This does not magically acknowledge a message after its channel has closed. That is no longer possible. It prevents a known infrastructure failure from becoming an unhandled application crash and makes the resulting message state visible.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;amqplib&lt;/code&gt; documentation also notes that a failed or closed channel is invalidated and that subsequent operations can throw. It recommends handling connection and channel events explicitly. See the &lt;a href="https://amqp-node.github.io/amqplib/channel_api.html" rel="noopener noreferrer"&gt;&lt;code&gt;amqplib&lt;/code&gt; channel API&lt;/a&gt; for the underlying behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skipping the Acknowledgement Is Not Message Loss
&lt;/h2&gt;

&lt;p&gt;It can feel uncomfortable to skip an acknowledgement after business logic completes. However, once the channel has closed, the consumer no longer controls that delivery through the normal acknowledgement path. With manual acknowledgements, RabbitMQ automatically requeues unacknowledged deliveries when their channel or connection closes. The message can then be delivered to the same consumer or another consumer after recovery.&lt;/p&gt;

&lt;p&gt;RabbitMQ documents this as part of its &lt;a href="https://www.rabbitmq.com/docs/confirms#automatic-requeueing" rel="noopener noreferrer"&gt;automatic requeueing behavior&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This protects the message, but it introduces another problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The business operation may already have succeeded.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The consumer receives &lt;code&gt;transfer-123&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It commits the transfer to the database.&lt;/li&gt;
&lt;li&gt;The RabbitMQ connection closes.&lt;/li&gt;
&lt;li&gt;The acknowledgement never reaches the broker.&lt;/li&gt;
&lt;li&gt;RabbitMQ requeues the message.&lt;/li&gt;
&lt;li&gt;Another consumer receives &lt;code&gt;transfer-123&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The transfer is processed again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The messaging system has behaved correctly. The message was never acknowledged, so RabbitMQ delivered it again.&lt;/p&gt;

&lt;p&gt;The duplicate side effect is now the application’s responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconnection Makes Idempotency Non-Optional
&lt;/h2&gt;

&lt;p&gt;RabbitMQ acknowledgements provide at-least-once delivery, not exactly-once business execution. A consumer must assume that any message can be delivered more than once, especially around connection loss. For transaction-sensitive workflows, this usually means giving every operation a stable idempotency key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransferMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingTransfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transferRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingTransfer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existingTransfer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transferRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transferReference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&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;A database uniqueness constraint should enforce this guarantee:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;transfers_reference_unique&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application-level check is useful, but the database constraint is the real protection against concurrent duplicate deliveries.&lt;/p&gt;

&lt;p&gt;For more complicated workflows, the consumer may need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store processed message identifiers.&lt;/li&gt;
&lt;li&gt;Make state transitions conditional.&lt;/li&gt;
&lt;li&gt;Use database transactions.&lt;/li&gt;
&lt;li&gt;Pass idempotency keys to external services.&lt;/li&gt;
&lt;li&gt;Record external-operation results before acknowledging.&lt;/li&gt;
&lt;li&gt;Design handlers so repeating them produces the same final state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RabbitMQ sets a redelivery flag when a message is delivered again. That is useful for logging and diagnosis, but it should not be the only duplicate-protection mechanism.&lt;/p&gt;

&lt;p&gt;A message can be duplicated in more than one way, and the consumer should be safe regardless of the flag.&lt;/p&gt;

&lt;p&gt;RabbitMQ’s &lt;a href="https://www.rabbitmq.com/docs/reliability" rel="noopener noreferrer"&gt;reliability guide&lt;/a&gt; recommends designing consumers to be idempotent because redelivery is an expected part of connection recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Catching &lt;code&gt;ack&lt;/code&gt; Errors Is Still Not a Delivery Guarantee
&lt;/h2&gt;

&lt;p&gt;There is a subtle limitation to the defensive wrapper.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;channel.ack(msg)&lt;/code&gt; does not throw, that only means the client accepted the operation locally. Consumer acknowledgements do not receive a corresponding promise or confirmation from &lt;code&gt;amqplib&lt;/code&gt;. The network could fail immediately after the acknowledgement is written locally but before RabbitMQ processes it.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Handle a channel already known to be unusable.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;protects the process from a known closed-channel error.&lt;/p&gt;

&lt;p&gt;It does not prove that the broker received the acknowledgement.&lt;/p&gt;

&lt;p&gt;That ambiguity is another reason idempotency must be part of the business workflow rather than an error-handling afterthought.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Not to Do
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do not acknowledge through the replacement channel
&lt;/h3&gt;

&lt;p&gt;The new channel does not own the old delivery. Delivery tags are scoped to their original channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not retry &lt;code&gt;ack&lt;/code&gt; indefinitely
&lt;/h3&gt;

&lt;p&gt;If the original channel is closed, repeatedly calling &lt;code&gt;ack&lt;/code&gt; cannot restore it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not silently swallow the error
&lt;/h3&gt;

&lt;p&gt;Avoiding a crash is useful, but losing all evidence of the event makes the system difficult to operate. Record the queue, message identifier, delivery tag, channel generation, and settlement action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not treat reconnection as successful recovery by itself
&lt;/h3&gt;

&lt;p&gt;A “connected” metric says nothing about handlers that were running during the outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not assume a successful handler ran only once
&lt;/h3&gt;

&lt;p&gt;The side effect may complete even when its acknowledgement does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test That Reveals the Problem
&lt;/h2&gt;

&lt;p&gt;This failure mode is straightforward to test, but ordinary integration tests rarely create the required timing.&lt;/p&gt;

&lt;p&gt;A useful test should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Publish a message with a known identifier.&lt;/li&gt;
&lt;li&gt;Start processing it with a deliberately slow handler.&lt;/li&gt;
&lt;li&gt;Allow the business side effect to complete.&lt;/li&gt;
&lt;li&gt;Restart RabbitMQ before the handler acknowledges.&lt;/li&gt;
&lt;li&gt;Allow the handler to reach its acknowledgement code.&lt;/li&gt;
&lt;li&gt;Wait for the consumer to reconnect.&lt;/li&gt;
&lt;li&gt;Observe the message being redelivered.&lt;/li&gt;
&lt;li&gt;Verify that the process remains alive.&lt;/li&gt;
&lt;li&gt;Verify that the business side effect exists only once.&lt;/li&gt;
&lt;li&gt;Verify that the consumer continues processing new messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The assertions matter more than whether the reconnect log appears.&lt;/p&gt;

&lt;p&gt;A successful test should prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The old handler cannot crash the process.&lt;/li&gt;
&lt;li&gt;The closed channel is never reused.&lt;/li&gt;
&lt;li&gt;The old delivery is not acknowledged through the new channel.&lt;/li&gt;
&lt;li&gt;The message can be redelivered safely.&lt;/li&gt;
&lt;li&gt;Duplicate execution does not duplicate the business result.&lt;/li&gt;
&lt;li&gt;The recovered consumer continues processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This test exercises the boundary between messaging reliability and business reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability Must Cover Both Timelines
&lt;/h2&gt;

&lt;p&gt;Connection metrics are necessary, but they are incomplete.&lt;/p&gt;

&lt;p&gt;For this failure mode, useful logs include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queue
messageId
deliveryTag
redelivered
channelGeneration
handlerStartedAt
handlerCompletedAt
channelClosedAt
settlementAction
settlementOutcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful metrics include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rabbitmq_consumer_reconnect_total
rabbitmq_delivery_redelivered_total
rabbitmq_settlement_skipped_total
rabbitmq_settlement_error_total
consumer_handler_duration
consumer_duplicate_operation_total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reconnect event tells you the future timeline recovered.&lt;/p&gt;

&lt;p&gt;A skipped acknowledgement, redelivered message, or duplicate-operation event tells you what happened to the current timeline.&lt;/p&gt;

&lt;p&gt;You need both views to understand the incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;A reconnecting RabbitMQ consumer is better than one that stays disconnected, but reconnection is only half of recovery.&lt;/p&gt;

&lt;p&gt;The harder problem is work that crosses the failure boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The message arrived through the old channel.&lt;/li&gt;
&lt;li&gt;The handler continued after that channel closed.&lt;/li&gt;
&lt;li&gt;The business operation may have completed.&lt;/li&gt;
&lt;li&gt;The acknowledgement may not have reached RabbitMQ.&lt;/li&gt;
&lt;li&gt;The message may therefore be delivered again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reliable consumer must associate every delivery with the channel that owns it, handle settlement operations defensively, expose uncertain outcomes through logs and metrics, and make business processing idempotent.&lt;/p&gt;

&lt;p&gt;The goal is not to pretend the failure never happened. The goal is to ensure that when infrastructure dies halfway through the work, the application fails predictably, recovers safely, and produces the correct business result even if the message returns.&lt;/p&gt;

&lt;p&gt;That is the difference between a consumer that reconnects and a messaging system that actually recovers.&lt;/p&gt;




</description>
      <category>eventdriven</category>
      <category>node</category>
      <category>softwareengineering</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Upgrading RabbitMQ Broker Taught Me About Building Reliable Messaging Systems</title>
      <dc:creator>Emmanuel Igunnu</dc:creator>
      <pubDate>Sat, 11 Jul 2026 12:40:11 +0000</pubDate>
      <link>https://dev.to/daemonicc/a-rabbitmq-upgrade-exposed-the-reliability-assumptions-hidden-in-our-messaging-system-3c77</link>
      <guid>https://dev.to/daemonicc/a-rabbitmq-upgrade-exposed-the-reliability-assumptions-hidden-in-our-messaging-system-3c77</guid>
      <description>&lt;p&gt;The RabbitMQ upgrade looked like a straightforward infrastructure task: move from RabbitMQ 3.X to 4.X, provision the new broker, review the client setup, confirm queues still declare correctly, restart consumers, watch the logs, and move on.&lt;/p&gt;

&lt;p&gt;But infrastructure upgrades rarely test only infrastructure. They also test the assumptions your application has been making for years. In this case, the upgrade forced a more important question: &lt;strong&gt;is our messaging system reliable by design, or has it simply been relying on stable conditions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters because a message queue can appear healthy when the broker is running, the network is stable, consumers are alive, and messages are acknowledged quickly. But production systems are not judged only by how they behave when everything is fine. They are judged by how they behave during restarts, closed channels, slow handlers, bad configuration, deployment windows, and partial failure.&lt;/p&gt;

&lt;p&gt;The RabbitMQ upgrade exposed those edges. It revealed assumptions around connection lifecycle, acknowledgements, dead-letter routing, retry behavior, observability, and operational simplicity. The real lesson was not just how to upgrade RabbitMQ. The real lesson was how to build a messaging layer that is easier to operate, easier to reason about, and safer to fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simplicity Is an Operational Feature
&lt;/h2&gt;

&lt;p&gt;One of the first things the upgrade exposed was complexity. Over time, messaging code can quietly become a small internal framework. A connection helper becomes a connection manager. A consumer wrapper becomes a consumer framework. Retry helpers appear, dead-letter helpers appear, failure handlers appear, and monitoring logic gets layered on top.&lt;/p&gt;

&lt;p&gt;Each addition may have been reasonable when introduced, but during an incident, complexity has a cost. Every abstraction becomes another place to inspect. Every helper becomes another assumption to validate. Every unused file becomes a possible source of false confidence.&lt;/p&gt;

&lt;p&gt;RabbitMQ integration code does not need to be clever. It needs to be understandable under pressure. The better design is usually boring: one clear connection manager, one reusable consumer abstraction, one predictable queue declaration path, one dead-letter queue convention, explicit retry behavior, minimal hidden magic, and no unused reliability code.&lt;/p&gt;

&lt;p&gt;This is not about writing less code for its own sake. It is about reducing operational surface area. When production is already under pressure, the questions should be easy to answer: is the app connected, is the channel open, is the consumer active, are messages being acknowledged, are failed messages going to the expected DLQ, and is the alert telling us the real problem?&lt;/p&gt;

&lt;p&gt;Simplicity is not just a code-quality preference. In infrastructure-facing code, simplicity is an operational feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  Queue Declarations Are Infrastructure Contracts
&lt;/h2&gt;

&lt;p&gt;Queue declaration logic deserves more respect than it often gets. It is easy to treat queue declarations as ordinary application setup code, but in RabbitMQ, a declaration is a contract with the broker.&lt;/p&gt;

&lt;p&gt;If a queue already exists with one set of arguments and an application later tries to declare it with different arguments, RabbitMQ can reject the declaration and close the channel. That means queue declarations are not harmless. They define durable infrastructure expectations.&lt;/p&gt;

&lt;p&gt;Every queue should have clear answers to a few basic questions. Who declares this queue? Is it durable? Does it have a dead-letter queue? What happens when a message is rejected? Are the arguments consistent across all services?&lt;/p&gt;

&lt;p&gt;This is where many messaging systems become fragile. Not because RabbitMQ is unreliable, but because different parts of the application slowly develop different ideas of what the same queue should be.&lt;/p&gt;

&lt;p&gt;The lesson here is that queue declarations should be treated like schema design or API contracts. They need consistency, ownership, and migration thinking. Changing a queue argument is not just a code change; it may also be an infrastructure migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dead-Letter Queues Should Be Boring
&lt;/h2&gt;

&lt;p&gt;Dead-letter queues are not the place for creativity. When a message fails, the path should be obvious.&lt;/p&gt;

&lt;p&gt;A queue named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;process_transfer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can dead-letter into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;process_transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dlq&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That naming convention is not impressive, and that is the point. The value of a DLQ naming convention is not elegance. It is predictability.&lt;/p&gt;

&lt;p&gt;When production is failing, an engineer should not have to search through configuration files to know where a failed message went. The queue name should tell them. The exact convention may differ across systems, but the principle should not: &lt;strong&gt;failed messages should have a predictable destination.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That predictability improves debugging, incident response, replay workflows, and operational confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconnection Is Required, But It Is Not Enough
&lt;/h2&gt;

&lt;p&gt;RabbitMQ can close connections for valid reasons. A broker restart, upgrade, failover, deployment, maintenance event, or admin action can produce something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONNECTION_FORCED - broker forced connection closure with reason 'shutdown'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should not surprise the application. A production consumer should assume the broker may disappear and come back. That means the consumer should recover when the initial connection fails, when the connection closes, when the channel closes, when the broker restarts, or when the network temporarily fails.&lt;/p&gt;

&lt;p&gt;The consumer should reconnect, recreate the channel, redeclare what it needs, reapply prefetch, and start consuming again. But reconnection alone does not solve everything.&lt;/p&gt;

&lt;p&gt;Reconnection protects future work. It does not automatically protect work already in progress. Consider this sequence: a consumer receives a message, the handler starts processing, RabbitMQ closes the connection, the application begins reconnecting, the original handler finishes, and then the handler tries to acknowledge or reject the message using the old channel.&lt;/p&gt;

&lt;p&gt;The application may now hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IllegalOperationError: Channel closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code may simply be doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the channel reference is stale. This is an important distinction because you can have reconnect logic and still crash when an in-flight handler tries to use a closed channel.&lt;/p&gt;

&lt;p&gt;That is why acknowledgement operations need defensive handling. If the channel is already closed, the application should log the problem clearly and avoid crashing. This does not mean silently ignoring message state. It means recognizing that once the channel is closed, the application no longer controls that delivery in the normal way. RabbitMQ will decide what happens based on connection loss, acknowledgement state, and queue configuration.&lt;/p&gt;

&lt;p&gt;The deeper lesson is that recovery has two timelines. There is the future timeline, where the consumer reconnects and continues. Then there is the current timeline, where existing handlers may still be completing work against dead infrastructure. A reliable consumer has to handle both.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prefetch Is a Risk Decision, Not Just a Throughput Setting
&lt;/h2&gt;

&lt;p&gt;RabbitMQ prefetch is often discussed as a performance tuning option, but it is more than that. Prefetch controls how many unacknowledged messages a consumer can hold at once, which affects throughput, fairness, failure recovery, duplicate risk, external pressure, and operational behavior during slowdowns.&lt;/p&gt;

&lt;p&gt;A conservative default like this is often safer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;prefetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means a consumer processes one unacknowledged message at a time. This may be the right default for workflows involving payments, wallet updates, settlements, transfers, inventory mutations, external API calls, and sensitive state transitions.&lt;/p&gt;

&lt;p&gt;The trade-off is clear. Lower prefetch may reduce throughput, but it also reduces the number of in-flight messages exposed to failure at any point in time. Higher prefetch can be appropriate for workloads that are idempotent, independent, fast, and safe to parallelize, but that should be an explicit choice.&lt;/p&gt;

&lt;p&gt;The better question is not only, “How do we process more messages?” The better question is: &lt;strong&gt;what level of concurrency can this workflow safely tolerate?&lt;/strong&gt; That question turns prefetch from a tuning knob into an engineering decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability Should Reduce Time to Diagnosis
&lt;/h2&gt;

&lt;p&gt;A vague alert is only slightly better than no alert. An alert like this tells you almost nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to connect to RabbitMQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It says a failure happened, but not why. During an incident, the first question is always: &lt;strong&gt;what failed, and what is the most likely cause?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A better alert includes the actual reason immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Failed to connect to RabbitMQ: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because many alerting tools show only the first line or preview text. If the useful error is hidden in expanded fields or logs, the alert slows people down.&lt;/p&gt;

&lt;p&gt;Good alerts should make it easier to distinguish between bad credentials, wrong host, DNS failure, connection timeout, TLS issues, broker restarts, missing environment variables, wrong protocol, wrong port, and network reachability issues. The best alert is not the one that simply says something broke. The best alert gives the responder a useful starting point.&lt;/p&gt;

&lt;p&gt;The same principle applies to monitoring. Monitoring should reduce uncertainty, not multiply symptoms. If RabbitMQ is unreachable, monitoring should not trigger several duplicate alerts for the same root cause. Observability should improve signal; it should not make an already noisy incident louder.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reliability Code That Does Not Run Is Not Reliability
&lt;/h2&gt;

&lt;p&gt;Unused code is bad, but unused reliability code is worse. A file named something like this looks reassuring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;consumer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It may include logic to read retry counts, republish messages, route failed messages to a DLQ, attach failure metadata, and decide whether a message should be retried. But if the active consumer does not call it, it is not part of the system’s reliability story. It is just code.&lt;/p&gt;

&lt;p&gt;Worse, it can mislead future engineers. Someone may see the file and assume failures are centrally handled. They may trust retry behavior that does not actually run. They may believe the system has a safety net that is not connected to anything.&lt;/p&gt;

&lt;p&gt;That is dangerous. Reliability code should be held to a higher standard than ordinary code. If it is not active, tested, observable, and part of the execution path, it should either be wired in properly or removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliability that only exists in the repository does not exist in production.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;Upgrading RabbitMQ from 3.X to 4.X was not just a broker upgrade. It was a reliability review.&lt;/p&gt;

&lt;p&gt;It exposed the difference between a messaging system that works and a messaging system that can be operated confidently. The biggest lessons were to keep the messaging layer simple, treat queue declarations as infrastructure contracts, make DLQ routing predictable, assume connections and channels will close, reconnect consumers automatically, protect in-flight acknowledgement paths, choose prefetch based on risk, make alerts useful from the first line, and remove unused reliability code.&lt;/p&gt;

&lt;p&gt;The best messaging setup is not the most abstract one. It is not the one with the most helper classes, and it is not the one that looks the most sophisticated in the codebase.&lt;/p&gt;

&lt;p&gt;The best setup is the one that keeps consuming, fails clearly, recovers predictably, and remains simple enough for engineers to understand when production is already under pressure.&lt;/p&gt;

&lt;p&gt;That is the real standard for reliable messaging systems.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>learning</category>
      <category>ampqlib</category>
      <category>pubsub</category>
    </item>
  </channel>
</rss>
