<?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: Factodus</title>
    <description>The latest articles on DEV Community by Factodus (@factodus).</description>
    <link>https://dev.to/factodus</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%2F4150245%2Fdc991a14-79b3-432c-a085-1ab914bf9ff1.png</url>
      <title>DEV Community: Factodus</title>
      <link>https://dev.to/factodus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/factodus"/>
    <language>en</language>
    <item>
      <title>Resubmitting Azure Service Bus dead letters without losing any</title>
      <dc:creator>Factodus</dc:creator>
      <pubDate>Tue, 29 Sep 2026 16:11:33 +0000</pubDate>
      <link>https://dev.to/factodus/resubmitting-azure-service-bus-dead-letters-without-losing-any-bkg</link>
      <guid>https://dev.to/factodus/resubmitting-azure-service-bus-dead-letters-without-losing-any-bkg</guid>
      <description>&lt;p&gt;A dead-letter queue (DLQ) is where Azure Service Bus parks messages that couldn’t be delivered or processed. Once you have fixed what made them fail, you usually want them processed after all. Service Bus has no “move back” command, so every way of resubmitting comes down to the same two steps: &lt;strong&gt;send a copy&lt;/strong&gt; to the queue or topic, then &lt;strong&gt;remove the original&lt;/strong&gt; from the DLQ. How you order and check those two steps decides whether you can lose messages or only duplicate them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short version.&lt;/strong&gt; Fix the cause first, or the messages will come straight back. In the portal, Service Bus Explorer’s &lt;strong&gt;Re-send selected messages&lt;/strong&gt; sends copies but leaves the originals in the DLQ. In code, receive from the DLQ in peek-lock mode, send a copy, and complete the original only after the send succeeded. That never loses a message, but a crash between the two steps can resend one, so consumers should be idempotent. Watch out for duplicate detection, which can silently drop the copy, and for subscriptions, where a resend to the topic reaches every subscription.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Fix the cause before you resubmit
&lt;/h2&gt;

&lt;p&gt;Read the dead-letter reason first (see &lt;a href="https://factodus.com/queue-studio/guides/peek-dead-letter-queue/" rel="noopener noreferrer"&gt;how to peek a dead-letter queue&lt;/a&gt;). Resubmitting a message that failed with &lt;code&gt;MaxDeliveryCountExceeded&lt;/code&gt; before the consumer bug is fixed just runs it through another full round of failed deliveries and back into the DLQ. Messages dead-lettered with &lt;code&gt;TTLExpiredException&lt;/code&gt; expire again unless consumers now keep up. &lt;code&gt;Session ID is null&lt;/code&gt; messages are dead-lettered again by a session-enabled queue until someone adds the session ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The portal: Re-send selected messages
&lt;/h2&gt;

&lt;p&gt;In the Azure portal, open the queue or subscription, then &lt;strong&gt;Service Bus Explorer&lt;/strong&gt; → &lt;strong&gt;Peek Mode&lt;/strong&gt; (or Receive Mode) → &lt;strong&gt;DeadLetter&lt;/strong&gt;, select messages and press &lt;strong&gt;Re-send selected messages&lt;/strong&gt;. You can edit the body and properties of each message before sending. It is the approach Microsoft suggests, and for a handful of messages it is the fastest. Keep two things in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The original stays in the DLQ.&lt;/strong&gt; Re-send sends a copy and deletes nothing. To remove the originals, receive them in peek-lock mode and &lt;strong&gt;Complete&lt;/strong&gt; them. A purge is not a shortcut here: it removes everything, including messages you haven’t resent yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From a subscription’s DLQ the copy goes to the topic&lt;/strong&gt;, so every subscription whose filter matches receives it, not only the one it failed in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. In code: send first, complete second
&lt;/h2&gt;

&lt;p&gt;The safe loop receives dead letters in peek-lock mode, which locks them without removing them. It sends each copy and completes the original only after the send returned. If anything fails in between, the original is still in the DLQ.&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="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.Identity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.Messaging.ServiceBus&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;client&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;ServiceBusClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"&amp;lt;namespace&amp;gt;.servicebus.windows.net"&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;DefaultAzureCredential&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;ServiceBusReceiver&lt;/span&gt; &lt;span class="n"&gt;dlq&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateReceiver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"orders"&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;ServiceBusReceiverOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;SubQueue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SubQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetter&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;ServiceBusSender&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;resubmitted&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="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceBusReceivedMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;batch&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;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReceiveMessagesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxWaitTime&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;FromSeconds&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&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;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceBusReceivedMessage&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&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;copy&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;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationProperties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OriginalDeadLetterReason"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetterReason&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;copy&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;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompleteMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only after the send succeeded&lt;/span&gt;
        &lt;span class="n"&gt;resubmitted&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="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Resubmitted &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resubmitted&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; messages"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the copy carries: &lt;code&gt;new ServiceBusMessage(receivedMessage)&lt;/code&gt; keeps the body, &lt;code&gt;MessageId&lt;/code&gt;, &lt;code&gt;SessionId&lt;/code&gt;, &lt;code&gt;CorrelationId&lt;/code&gt;, &lt;code&gt;Subject&lt;/code&gt;, &lt;code&gt;ContentType&lt;/code&gt;, time to live and application properties. It drops what the broker owns: the sequence number, enqueued time, scheduled enqueue time, delivery count, and the &lt;code&gt;DeadLetterReason&lt;/code&gt; and &lt;code&gt;DeadLetterErrorDescription&lt;/code&gt; properties. That is why the sample copies the reason into a property of its own.&lt;/p&gt;

&lt;p&gt;Why this order: if the process dies after the send and before the complete, the original stays in the DLQ and the next run sends it again. You get a duplicate, never a loss. The reverse order (complete first, then send) loses the message whenever the send fails. The same applies to locks: 50 sends take well under the default one-minute lock, but if a lock expires anyway, the complete throws, the original remains and the next run resends it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Duplicate detection can swallow the copy
&lt;/h2&gt;

&lt;p&gt;If the target queue or topic has &lt;strong&gt;duplicate detection&lt;/strong&gt; enabled, Service Bus drops any message whose &lt;code&gt;MessageId&lt;/code&gt; it has already seen within the detection window, which can be up to seven days. The copy keeps the original &lt;code&gt;MessageId&lt;/code&gt;. A message that is resubmitted soon after it was first sent disappears without an error. If that matters, give the copy a new &lt;code&gt;MessageId&lt;/code&gt; and keep the old one in a property:&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="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationProperties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OriginalMessageId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a trade-off. Consumers that deduplicate by &lt;code&gt;MessageId&lt;/code&gt; will then treat the copy as a new message.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Order and sessions
&lt;/h2&gt;

&lt;p&gt;A resubmitted message gets a new sequence number, so it is processed &lt;strong&gt;after&lt;/strong&gt; everything that arrived while it sat in the DLQ. In a session-enabled queue the copy keeps its &lt;code&gt;SessionId&lt;/code&gt;, which it needs because such a queue dead-letters messages without one, and it lands at the end of that session. If your consumers rely on the order within a session, resubmitting an old event behind newer ones can undo state. Check such messages by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Subscriptions: you can’t send to one subscription
&lt;/h2&gt;

&lt;p&gt;Messages are sent to topics, never to subscriptions. A dead letter from subscription &lt;code&gt;invoices&lt;/code&gt; resubmitted to its topic reaches every subscription whose filter matches it, including ones that processed it the first time. The options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make every consumer of the topic idempotent, so the extra copies do nothing. This is the cleanest fix.&lt;/li&gt;
&lt;li&gt;Resubmit into a dedicated queue that only the failing consumer reads.&lt;/li&gt;
&lt;li&gt;Add a property such as &lt;code&gt;ReplayFor = 'invoices'&lt;/code&gt; and have the other subscriptions’ filters exclude it. This works but touches every subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From VS Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://factodus.com/queue-studio/#service-bus" rel="noopener noreferrer"&gt;Queue Studio for Service Bus&lt;/a&gt; (an extension I build) runs this whole procedure as one command. In Pro, &lt;strong&gt;Move All Back&lt;/strong&gt; on a dead-letter queue sends the messages to the queue they came from, to the topic for a subscription, or to any queue or topic you pick. Each batch is sent first and removed from the DLQ only after the send succeeded. Progress is shown and the move can be cancelled. The guarantee is the same as the loop above: nothing is lost, and an interrupted move can leave a duplicate.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://factodus.com/queue-studio/guides/resubmit-dead-letter-messages/" rel="noopener noreferrer"&gt;factodus.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>servicebus</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
