Service Bus Scheduled & Deferred Messages

Time-Based Message Delivery and Conditional Processing


Introduction

Azure Service Bus provides two powerful mechanisms for controlling when messages are processed: Scheduled Messages and Deferred Messages. Scheduled messages allow you to queue messages for future delivery at a specific time, while deferred messages enable conditional processing where messages are temporarily set aside and retrieved later.

This guide covers:

  • Scheduled messages — Delay message delivery to a specific time
  • Deferred messages — Temporarily defer processing and retrieve later
  • Use cases — Common patterns and implementations
  • Best practices — Efficient usage patterns

Scheduled Messages

How Scheduling Works

┌─────────────────────────────────────────────────────────────────────┐
│                    SCHEDULED MESSAGES                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   Time ─────────────────────────────────────────────────────────▶   │
│                                                                     │
│   ┌─────────┐    Scheduled      ┌─────────┐     Delivered           │
│   │ Message │ ──▶ Enqueue       │ Message │ ──▶ to queue            │
│   │ Created │    Time:          │ in      │     at scheduled        │
│   │  10:00  │    12:00          │ Queue   │     time                │
│   └─────────┘                   └─────────┘                         │
│                                                                     │
│   ScheduledEnqueueTime: When message becomes available              │
│   Message stays hidden until scheduled time                         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Schedule for Future Time

var sender = client.CreateSender("orders-queue");

var message = new ServiceBusMessage("Process this order")
{
    ContentType = "application/json",
    MessageId = Guid.NewGuid().ToString()
};

// Schedule for 5 minutes from now
message.ScheduledEnqueueTime = DateTimeOffset.Now.AddMinutes(5);

await sender.SendMessageAsync(message);

Schedule for Specific DateTime

// Schedule for specific time
var scheduleTime = new DateTimeOffset(
    DateTime.Today.AddHours(14),  // 2 PM today
    TimeSpan.FromHours(-5)         // EST timezone
);

var message = new ServiceBusMessage("Process at 2 PM")
{
    ScheduledEnqueueTime = scheduleTime
};

await sender.SendMessageAsync(message);

// Schedule for tomorrow
var tomorrowAt9AM = DateTime.Today.AddDays(1).AddHours(9);
var tomorrowSchedule = new DateTimeOffset(tomorrowAt9AM, TimeSpan.FromHours(-5));

var tomorrowMessage = new ServiceBusMessage("Process tomorrow at 9 AM")
{
    ScheduledEnqueueTime = tomorrowSchedule
};

await sender.SendMessageAsync(tomorrowMessage);

Cancel Scheduled Message

// Get scheduled messages
var scheduledMessages = await sender.ScheduleMessagesAsync(
    new ServiceBusMessage("Test"),
    DateTimeOffset.Now.AddHours(1)
);

// Cancel using sequence number
await sender.CancelScheduledMessageAsync(scheduledMessages[0]);

Multiple Scheduled Messages

// Schedule multiple messages at once
var messages = new List<ServiceBusMessage>();

for (int i = 0; i < 10; i++)
{
    var message = new ServiceBusMessage($"Reminder {i}")
    {
        // Each message scheduled 1 hour apart
        ScheduledEnqueueTime = DateTimeOffset.Now.AddHours(i)
    };
    messages.Add(message);
}

await sender.SendMessagesAsync(messages);

Deferred Messages

How Deferral Works

┌─────────────────────────────────────────────────────────────────────┐
│                    DEFERRED MESSAGES                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────┐         ┌─────────┐         ┌─────────┐               │
│   │ Message │ Process │  Defer  │  Store  │Receive  │               │
│   │Received │         │ Message │ Sequence│Deferred │               │
│   └────┬────┘         └────┬────┘    #    └────┬────┘               │
│        │                  │                   │                     │
│        ▼                  ▼                   ▼                     │
│   ┌─────────┐       ┌───────────┐        ┌──────────┐               │
│   │ Can't   │       │ Message   │        │ Message  │               │
│   │ process │       │ moved to  │        │ retrieved│               │
│   │ now     │       │ hidden    │        │ by seq # │               │
│   └─────────┘       │ queue     │        └──────────┘               │
│                     └───────────┘                                   │
│                                                                     │
│   Save sequence number to retrieve later                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Defer a Message

var processor = client.CreateProcessor("orders-queue");

processor.ProcessMessageAsync += async args =>
{
    var message = args.Message;
    
    // Determine if we need to defer
    if (message.Properties.TryGetValue("RequiresExternalData", out var requiresData) && 
        (bool)requiresData == true)
    {
        // Cannot process yet - defer message
        await args.DeferAsync();
        
        // Store sequence number for later retrieval
        await StoreSequenceNumberAsync(
            message.SessionId, 
            message.SequenceNumber);
        
        return;
    }
    
    // Process normally
    await ProcessOrderAsync(message);
    await args.CompleteMessageAsync(message);
};

Retrieve Deferred Messages

// Get sequence numbers we stored earlier
var sequenceNumbers = await GetStoredSequenceNumbersAsync();

var receiver = client.CreateReceiver("orders-queue");

// Receive all deferred messages
var deferredMessages = await receiver.ReceiveDeferredMessagesAsync(sequenceNumbers);

foreach (var deferred in deferredMessages)
{
    // Process deferred message
    await ProcessOrderAsync(deferred);
    await receiver.CompleteMessageAsync(deferred.LockToken);
}

Defer Based on Condition

processor.ProcessMessageAsync += async args =>
{
    var message = args.Message;
    var order = JsonSerializer.Deserialize<Order>(message.Body);
    
    // Check if we have all required data
    if (!await HasInventoryAsync(order.Items))
    {
        // Inventory not available - defer
        var sequenceNumber = message.SequenceNumber;
        await args.DeferAsync();
        
        // Schedule a retry after 5 minutes
        var retryMessage = new ServiceBusMessage(message.Body)
        {
            ScheduledEnqueueTime = DateTimeOffset.Now.AddMinutes(5)
        };
        retryMessage.Properties["OriginalSequenceNumber"] = sequenceNumber;
        
        await sender.SendMessageAsync(retryMessage);
        return;
    }
    
    // Process if inventory available
    await ProcessOrderAsync(order);
    await args.CompleteMessageAsync(message);
};

Use Cases and Patterns

Pattern 1: Retry After Delay

public class OrderProcessingService
{
    public async Task ProcessWithRetryAsync(ProcessMessageEventArgs args)
    {
        var message = args.Message;
        
        try
        {
            // Try to process
            await ProcessOrderAsync(message);
            await args.CompleteMessageAsync(message);
        }
        catch (TransientException ex)
        {
            // Failed - retry after delay
            var retryCount = (int)(message.ApplicationProperties.GetValueOrDefault("RetryCount", 0));
            
            if (retryCount >= 3)
            {
                // Too many retries - dead letter
                await args.DeadLetterAsync("MaxRetriesExceeded", ex.Message);
                return;
            }
            
            // Defer for retry
            await args.DeferAsync();
            
            // Schedule retry message
            var retryMessage = new ServiceBusMessage(message.Body)
            {
                ScheduledEnqueueTime = DateTimeOffset.Now.AddMinutes(5)
            };
            retryMessage.Properties["RetryCount"] = retryCount + 1;
            
            await sender.SendMessageAsync(retryMessage);
        }
    }
}

Pattern 2: Wait for External Data

processor.ProcessMessageAsync += async args =>
{
    var order = JsonSerializer.Deserialize<Order>(args.Message.Body);
    
    // Check if we need customer verification
    if (order.RequiresVerification)
    {
        // Defer the message
        await args.DeferAsync();
        
        // Store sequence number for later
        await _cache.SetAsync(
            $"deferred_order_{order.OrderId}",
            args.Message.SequenceNumber.ToString());
        
        // Trigger external verification
        await TriggerVerificationAsync(order.CustomerId);
        
        return;
    }
    
    // No verification needed
    await ProcessOrderAsync(order);
    await args.CompleteMessageAsync(args.Message);
};

// Later - when verification completes
public async Task HandleVerificationCompletedAsync(string customerId)
{
    var sequenceNumberStr = await _cache.GetAsync($"deferred_order_{customerId}");
    if (long.TryParse(sequenceNumberStr, out var sequenceNumber))
    {
        var receiver = client.CreateReceiver("orders-queue");
        var messages = await receiver.ReceiveDeferredMessagesAsync(new[] { sequenceNumber });
        
        if (messages.Any())
        {
            var order = JsonSerializer.Deserialize<Order>(messages[0].Body);
            await ProcessOrderAsync(order);
            await receiver.CompleteMessageAsync(messages[0].LockToken);
        }
    }
}

Pattern 3: Scheduled Notifications

public class NotificationScheduler
{
    public async Task ScheduleReminderAsync(Reminder reminder)
    {
        var sender = client.CreateSender("notifications-queue");
        
        var message = new ServiceBusMessage(JsonSerializer.Serialize(reminder))
        {
            Subject = "Reminder",
            MessageId = reminder.ReminderId
        };
        
        // Schedule for the reminder time
        message.ScheduledEnqueueTime = reminder.ReminderTime;
        
        await sender.SendMessageAsync(message);
    }
}

// Process scheduled notifications
var notificationProcessor = client.CreateProcessor("notifications-queue");

notificationProcessor.ProcessMessageAsync += async args =>
{
    var notification = JsonSerializer.Deserialize<Reminder>(args.Message.Body);
    
    await SendNotificationAsync(notification);
    await args.CompleteMessageAsync(args.Message);
};

Best Practices

PracticeDescription
Track sequence numbersStore deferred message sequence numbers
Set reasonable TTLScheduled messages expire if not delivered
Handle time zonesUse DateTimeOffset for scheduling
Cancel unused schedulesRemove scheduled messages no longer needed
Monitor deferred backlogTrack how many messages are deferred

Monitoring

# Get scheduled message count
az servicebus queue show \
  --name orders-queue \
  --namespace-name mynamespace \
  --resource-group myrg \
  --query "scheduledMessageCount"

Related Topics


Azure Integration Hub - Intermediate Level