Service Bus Receiving Messages

Receiving messages from Azure Service Bus means reading messages from a queue or subscription, processing them in the application, and confirming the result back to Service Bus. The confirmation step is critical — it tells Service Bus whether to delete the message or retry it. This topic covers both push-based and pull-based receiving models, message settlement options, and best practices.

Two Ways to Receive Messages

MethodHow It WorksBest For
Pull (Polling)Application calls ReceiveMessageAsync() on demandLow-volume, on-demand processing
Push (Processor)Service Bus calls a handler function automatically when a message arrivesHigh-volume, continuous processing

Receive Modes

ModeBehaviorRisk
Peek-Lock (default)Lock the message, process it, then settle itLow — message is safe until explicitly settled
Receive-and-DeleteDelete the message the moment it is receivedHigh — if the app crashes mid-processing, message is lost

Message Settlement Options

After reading a message in Peek-Lock mode, the receiver must settle it. Settlement tells Service Bus what to do with the message next.

Settlement ActionMethodResult
CompleteCompleteMessageAsync()Message is deleted from queue — success
AbandonAbandonMessageAsync()Message is returned to queue — another consumer can retry
Dead-LetterDeadLetterMessageAsync()Message moves to Dead Letter Queue — no more retries
DeferDeferMessageAsync()Message is parked with its SequenceNumber — must be fetched explicitly later
Receiver reads message (lock starts)
        |
        | App processes message
        |
        |-- SUCCESS --> CompleteMessageAsync() --> Message DELETED
        |
        |-- RETRY   --> AbandonMessageAsync()  --> Message back in QUEUE
        |
        |-- POISON  --> DeadLetterMessageAsync() --> Message in DLQ
        |
        '-- WAIT    --> DeferMessageAsync()     --> Message PARKED (fetch later)

Pull-Based Receiving — .NET SDK

Receive a Single Message

using Azure.Messaging.ServiceBus;

string connectionString = "Endpoint=sb://myshopns.servicebus.windows.net/;...";
string queueName = "orders";

await using var client = new ServiceBusClient(connectionString);
await using var receiver = client.CreateReceiver(queueName);

// Wait up to 5 seconds for a message
ServiceBusReceivedMessage message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));

if (message != null)
{
    Console.WriteLine($"Received: {message.Body}");
    Console.WriteLine($"MessageId: {message.MessageId}");
    Console.WriteLine($"DeliveryCount: {message.DeliveryCount}");

    // Mark message as successfully processed
    await receiver.CompleteMessageAsync(message);
    Console.WriteLine("Message completed.");
}
else
{
    Console.WriteLine("No messages in queue.");
}

Receive Multiple Messages (Batch Pull)

// Receive up to 10 messages at once
IReadOnlyList<ServiceBusReceivedMessage> messages =
    await receiver.ReceiveMessagesAsync(maxMessages: 10, maxWaitTime: TimeSpan.FromSeconds(5));

foreach (var msg in messages)
{
    try
    {
        Console.WriteLine($"Processing: {msg.Body}");
        // ... business logic ...
        await receiver.CompleteMessageAsync(msg);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}. Abandoning message.");
        await receiver.AbandonMessageAsync(msg);
    }
}

Receive from a Topic Subscription

// Receive from a specific subscription
await using var receiver = client.CreateReceiver("order-events", "inventory-sub");

var message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
if (message != null)
{
    Console.WriteLine($"Order event: {message.Body}");
    await receiver.CompleteMessageAsync(message);
}

Push-Based Receiving — ServiceBusProcessor (.NET)

The ServiceBusProcessor continuously listens for messages and calls a handler function each time a message arrives. This is the recommended approach for production services.

await using var client = new ServiceBusClient(connectionString);

var processorOptions = new ServiceBusProcessorOptions
{
    MaxConcurrentCalls    = 5,   // Process up to 5 messages simultaneously
    AutoCompleteMessages  = false // Manually complete messages for safety
};

await using var processor = client.CreateProcessor("orders", processorOptions);

// Handler called when a message arrives
processor.ProcessMessageAsync += async args =>
{
    var body = args.Message.Body.ToString();
    Console.WriteLine($"Processing order: {body}");

    // Business logic here...

    await args.CompleteMessageAsync(args.Message);
    Console.WriteLine("Message completed.");
};

// Handler called when an error occurs
processor.ProcessErrorAsync += async args =>
{
    Console.WriteLine($"Error: {args.Exception.Message}");
    Console.WriteLine($"Source: {args.ErrorSource}");
    await Task.CompletedTask;
};

await processor.StartProcessingAsync();
Console.WriteLine("Processor running. Press Enter to stop.");
Console.ReadLine();
await processor.StopProcessingAsync();

How the Processor Works

[Queue: orders]
      |
      | Service Bus pushes messages automatically
      v
[ServiceBusProcessor]
      |
      |-- message 1 --> [ProcessMessageAsync handler] --> Complete
      |-- message 2 --> [ProcessMessageAsync handler] --> Complete
      |-- message 3 --> [ProcessMessageAsync handler] --> Abandon (retry)
      |
      |-- error occurs --> [ProcessErrorAsync handler] --> log error

Pull-Based Receiving — Python SDK

Receive a Single Message

from azure.servicebus import ServiceBusClient

connection_str = "Endpoint=sb://myshopns.servicebus.windows.net/;..."
queue_name = "orders"

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:
            print(f"Received: {str(msg)}")
            receiver.complete_message(msg)
            break  # Process one message then stop

Receive and Abandon on Failure — Python

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:
            try:
                print(f"Processing: {str(msg)}")
                # Business logic...
                receiver.complete_message(msg)
            except Exception as e:
                print(f"Error: {e}. Abandoning.")
                receiver.abandon_message(msg)

Message Lock Renewal

The default lock duration is 60 seconds. If processing takes longer, the lock expires and another consumer can pick up the same message — causing duplicate processing. Use lock renewal to extend the lock while processing continues.

// .NET — renew lock every 30 seconds during long processing
var message = await receiver.ReceiveMessageAsync();

using var cts = new CancellationTokenSource();
var renewTask = Task.Run(async () =>
{
    while (!cts.Token.IsCancellationRequested)
    {
        await Task.Delay(TimeSpan.FromSeconds(30), cts.Token);
        await receiver.RenewMessageLockAsync(message);
        Console.WriteLine("Lock renewed.");
    }
});

// Simulate long processing
await Task.Delay(TimeSpan.FromMinutes(3));
cts.Cancel();

await receiver.CompleteMessageAsync(message);

Dead-Lettering a Message Manually

var message = await receiver.ReceiveMessageAsync();

if (message.Body.ToString().Contains("INVALID"))
{
    await receiver.DeadLetterMessageAsync(
        message,
        deadLetterReason: "InvalidPayload",
        deadLetterErrorDescription: "Message body failed schema validation."
    );
    Console.WriteLine("Message dead-lettered.");
}

Receiving Best Practices

  • Always use Peek-Lock mode for critical messages — avoid Receive-and-Delete
  • Set AutoCompleteMessages = false on the processor — complete messages manually after successful processing
  • Implement the ProcessErrorAsync handler to log and alert on errors
  • Use MaxConcurrentCalls to control the concurrency level on the processor
  • Renew message locks when processing time exceeds the lock duration
  • Dead-letter messages that fail validation immediately — do not keep retrying invalid messages
  • Monitor the DeliveryCount property to detect frequently failing messages

Summary

Azure Service Bus supports pull-based and push-based message receiving. Pull-based receiving uses ReceiveMessageAsync for on-demand processing. Push-based receiving uses the ServiceBusProcessor for continuous, concurrent message handling. After reading a message in Peek-Lock mode, the receiver settles it by completing, abandoning, dead-lettering, or deferring it. Lock renewal prevents premature lock expiry during long-running processing. Proper settlement and error handling ensure reliable message delivery without data loss.

Leave a Comment