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

ActionEffectWho Can Receive It Next
AbandonMessage returns to the queue as ActiveAny available consumer
DeferMessage stays in queue with Deferred stateOnly the consumer that knows the SequenceNumber
Dead-LetterMessage moves to Dead Letter QueueOnly consumers reading the DLQ
CompleteMessage is permanently deletedNobody — it is gone

When to Use Message Deferral

Use CaseWhy Deferral Helps
Out-of-Order ArrivalStep 3 arrives before Step 1 — defer Step 3 until Steps 1 and 2 are done
Waiting for External DataPayment message arrives but product data is not yet ready — defer until data syncs
Dependency on Another MessageA reconciliation message must wait until all sub-messages are received
User Approval RequiredA 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

PropertyValue in Deferred State
StateDeferred
DeliveryCountIncrements each time it was received before deferral
VisibilityHidden from normal receivers — only fetchable by SequenceNumber
LockReleased — no lock held on a deferred message
TTLContinues counting — message can expire while deferred
Max Delivery CountStill 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.

Leave a Comment