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
| Practice | Description |
|---|---|
| Track sequence numbers | Store deferred message sequence numbers |
| Set reasonable TTL | Scheduled messages expire if not delivered |
| Handle time zones | Use DateTimeOffset for scheduling |
| Cancel unused schedules | Remove scheduled messages no longer needed |
| Monitor deferred backlog | Track 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
- Dead-Letter Queue — Handling failed messages
- Sessions — Ordered processing
- Retry Policies — Automatic retries
Azure Integration Hub - Intermediate Level