Service Bus Message Deferral
Azure Service Bus Message Deferral allows a consumer to temporarily park a message without completing or abandoning it. The message is set aside with a special state and can only be retrieved later using its SequenceNumber. Deferral is not a retry mechanism — it is an intentional postponement for messages that a consumer is not yet ready to process.
What Is Deferral?
When a consumer reads a message and realizes it cannot process it right now — but does not want to abandon it (which would put it back in the queue for any consumer) — it can defer it. A deferred message stays in the queue with a Deferred state and becomes invisible to normal receivers. Only the consumer that deferred it can retrieve it, and only by using the SequenceNumber.
Normal message lifecycle:
Received --> Process --> Complete (deleted)
--> Process fails --> Abandon (back to queue)
Deferred message lifecycle:
Received --> Not ready yet --> Defer (parked with SequenceNumber)
--> Later, fetch by SequenceNumber --> Process --> Complete
Deferral vs Abandon — Key Difference
| Action | Effect | Who Can Receive It Next |
|---|---|---|
| Abandon | Message returns to the queue as Active | Any available consumer |
| Defer | Message stays in queue with Deferred state | Only the consumer that knows the SequenceNumber |
| Dead-Letter | Message moves to Dead Letter Queue | Only consumers reading the DLQ |
| Complete | Message is permanently deleted | Nobody — it is gone |
When to Use Message Deferral
| Use Case | Why Deferral Helps |
|---|---|
| Out-of-Order Arrival | Step 3 arrives before Step 1 — defer Step 3 until Steps 1 and 2 are done |
| Waiting for External Data | Payment message arrives but product data is not yet ready — defer until data syncs |
| Dependency on Another Message | A reconciliation message must wait until all sub-messages are received |
| User Approval Required | A large order message awaits manager approval before processing |
Deferral Flow Diagram
Queue: order-steps (Session disabled for simplicity)
Messages arrive: [Step3] [Step1] [Step2]
(out of order)
Consumer reads [Step3]:
- Step 1 not done yet
- DeferMessageAsync(Step3) --> Deferred State (SequenceNumber = 5050)
- Save SequenceNumber 5050 somewhere (e.g., database)
Consumer reads [Step1]:
- Process Step1 successfully --> Complete
Consumer reads [Step2]:
- Process Step2 successfully --> Complete
Now Step3 can be processed:
- Fetch deferred message using SequenceNumber 5050
- Process Step3 --> Complete
Deferring a Message — .NET SDK
using Azure.Messaging.ServiceBus;
await using var client = new ServiceBusClient(connectionString);
await using var receiver = client.CreateReceiver("order-steps");
var message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
if (message != null)
{
string body = message.Body.ToString();
Console.WriteLine($"Received: {body}");
if (body.Contains("Step3") && !Step1Completed)
{
// Not ready to process Step3 yet
await receiver.DeferMessageAsync(message);
long deferredSeqNo = message.SequenceNumber;
Console.WriteLine($"Message deferred. SequenceNumber = {deferredSeqNo}");
// Save deferredSeqNo to database for later retrieval
SaveSequenceNumber("step3", deferredSeqNo);
}
else
{
// Process normally
Console.WriteLine($"Processing: {body}");
await receiver.CompleteMessageAsync(message);
}
}
Receiving a Deferred Message — .NET SDK
// Later — after prerequisites are met, retrieve the deferred message
await using var receiver = client.CreateReceiver("order-steps");
// Retrieve the saved SequenceNumber
long sequenceNumber = GetSequenceNumber("step3"); // from database
// Fetch the specific deferred message by SequenceNumber
ServiceBusReceivedMessage deferredMsg =
await receiver.ReceiveDeferredMessageAsync(sequenceNumber);
if (deferredMsg != null)
{
Console.WriteLine($"Processing deferred message: {deferredMsg.Body}");
// Business logic here...
await receiver.CompleteMessageAsync(deferredMsg);
Console.WriteLine("Deferred message processed and completed.");
}
Deferring in a Session Queue
In session-enabled queues, the consumer that owns the session must also retrieve the deferred message. The session receiver uses ReceiveDeferredMessageAsync() on the active session.
await using var sessionReceiver = await client.AcceptSessionAsync(
"order-steps-session-queue",
sessionId: "order-101"
);
var msg = await sessionReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
if (msg != null && msg.Body.ToString().Contains("Step3"))
{
await sessionReceiver.DeferMessageAsync(msg);
long seqNo = msg.SequenceNumber;
Console.WriteLine($"Deferred within session. SeqNo: {seqNo}");
// Later, within the same session:
var deferred = await sessionReceiver.ReceiveDeferredMessageAsync(seqNo);
await sessionReceiver.CompleteMessageAsync(deferred);
}
Python SDK — Deferral
from azure.servicebus import ServiceBusClient
connection_str = "Endpoint=sb://myshopns.servicebus.windows.net/;..."
queue_name = "order-steps"
with ServiceBusClient.from_connection_string(connection_str) as client:
with client.get_queue_receiver(queue_name, max_wait_time=5) as receiver:
for msg in receiver:
body = str(msg)
print(f"Received: {body}")
if "Step3" in body:
receiver.defer_message(msg)
seq_no = msg.sequence_number
print(f"Deferred. SequenceNumber: {seq_no}")
break
else:
receiver.complete_message(msg)
# Retrieve deferred message later
with client.get_queue_receiver(queue_name) as receiver:
deferred = receiver.receive_deferred_messages([seq_no])
for msg in deferred:
print(f"Processing deferred: {str(msg)}")
receiver.complete_message(msg)
Deferred Message Properties
| Property | Value in Deferred State |
|---|---|
| State | Deferred |
| DeliveryCount | Increments each time it was received before deferral |
| Visibility | Hidden from normal receivers — only fetchable by SequenceNumber |
| Lock | Released — no lock held on a deferred message |
| TTL | Continues counting — message can expire while deferred |
| Max Delivery Count | Still applies — too many deferrals can lead to dead-lettering |
Important Warning — TTL Still Applies
Message TTL = 30 minutes
Message deferred at: 10:00 AM
10:00 AM: Deferred. SequenceNumber = 5050 saved.
10:30 AM: TTL expires.
If consumer tries to retrieve at 10:35 AM:
--> Message is GONE (expired while deferred)
FIX: Extend TTL when deferring messages that need long wait times.
Or use dead-lettering on expiry to catch them in DLQ.
Store SequenceNumber Safely
The SequenceNumber is the only way to retrieve a deferred message. Losing it means the message is permanently unreachable (until it expires or gets dead-lettered). Always persist SequenceNumbers in a reliable store like Azure SQL, Cosmos DB, or Azure Table Storage.
Deferred Message Tracking Table (example): | SessionId | Step | SequenceNumber | DeferredAt | Status | |-------------|--------|----------------|---------------------|----------| | order-101 | Step3 | 5050 | 2024-01-15 10:00 AM | Pending | | order-102 | Step2 | 5051 | 2024-01-15 10:01 AM | Retrieved|
Summary
Message Deferral in Azure Service Bus parks a received message in a Deferred state, making it invisible to normal consumers. The message is retrieved later using its SequenceNumber. Deferral is ideal for out-of-order arrivals, dependency management, and messages awaiting approval or external data. Unlike Abandon, a deferred message can only be retrieved by a consumer that explicitly knows its SequenceNumber. The SequenceNumber must be stored persistently since its loss makes the deferred message permanently inaccessible until it expires.
