Service Bus Scheduled Messages
Azure Service Bus Scheduled Messages allow senders to place a message in a queue or topic but delay its visibility until a specific future time. The message is stored immediately but only becomes available to consumers at the scheduled time. This is useful for reminder systems, delayed order processing, retry workflows, and time-triggered business processes.
How Scheduled Messages Work
Sender sends message at 10:00 AM --> ScheduledEnqueueTime = 10:30 AM 10:00 AM: Message stored in queue (state = Scheduled, invisible to consumers) 10:30 AM: Message becomes visible (state = Active) 10:30 AM: Consumer picks up and processes the message
Scheduled vs Non-Scheduled Message
| Type | Available to Consumer | Stored Immediately |
|---|---|---|
| Regular Message | Immediately | Yes |
| Scheduled Message | Only at the ScheduledEnqueueTime | Yes (but invisible) |
Real-World Use Cases
| Use Case | Example |
|---|---|
| Reminder Notifications | Send an email reminder 24 hours after a cart abandonment |
| Delayed Order Processing | Process an order at 9 AM the next business day |
| Subscription Renewal Alerts | Send a renewal reminder 7 days before expiry |
| Rate Limiting / Throttling | Schedule a retry message 5 minutes from now after a failure |
| Campaign Scheduling | Trigger a promotional email at 8 AM on a campaign launch date |
| Delayed Cancellation | Cancel an order 30 minutes after creation if payment is not received |
Method 1 — Set ScheduledEnqueueTime on the Message
Set the ScheduledEnqueueTime property directly on the message before sending. This is the simplest approach.
using Azure.Messaging.ServiceBus;
await using var client = new ServiceBusClient(connectionString);
await using var sender = client.CreateSender("orders");
var message = new ServiceBusMessage("Process this order at 11 PM tonight")
{
MessageId = "order-101-scheduled",
Subject = "ScheduledOrder",
ScheduledEnqueueTime = DateTimeOffset.UtcNow.AddHours(8) // 8 hours from now
};
await sender.SendMessageAsync(message);
Console.WriteLine($"Message scheduled for: {message.ScheduledEnqueueTime}");
Method 2 — ScheduleMessageAsync (Returns Sequence Number)
Use ScheduleMessageAsync() to schedule a message and receive a SequenceNumber in return. The SequenceNumber allows cancellation of the scheduled message before it becomes active.
await using var sender = client.CreateSender("orders");
var message = new ServiceBusMessage("Reminder: Your session starts in 15 minutes.")
{
MessageId = "reminder-456"
};
DateTimeOffset deliveryTime = DateTimeOffset.UtcNow.AddMinutes(15);
// Schedule and capture the SequenceNumber
long sequenceNumber = await sender.ScheduleMessageAsync(message, deliveryTime);
Console.WriteLine($"Message scheduled. SequenceNumber: {sequenceNumber}");
Console.WriteLine($"Will be visible at: {deliveryTime}");
Cancel a Scheduled Message
A scheduled message can be cancelled before its delivery time using the SequenceNumber returned during scheduling.
// Cancel the scheduled message using the stored sequence number
await sender.CancelScheduledMessageAsync(sequenceNumber);
Console.WriteLine($"Scheduled message {sequenceNumber} cancelled successfully.");
Schedule and Cancel Flow
10:00 AM: Message scheduled for 10:30 AM
SequenceNumber = 9876 returned
10:20 AM: Decision made to cancel
CancelScheduledMessageAsync(9876) called
10:30 AM: Message does NOT appear in the queue (successfully cancelled)
Schedule Multiple Messages at Once
await using var sender = client.CreateSender("notifications");
var messages = new List<ServiceBusMessage>
{
new ServiceBusMessage("Reminder: 1 hour before meeting") { MessageId = "r-1hr" },
new ServiceBusMessage("Reminder: 15 mins before meeting") { MessageId = "r-15m" },
new ServiceBusMessage("Meeting started!") { MessageId = "r-now" }
};
var deliveryTimes = new[]
{
DateTimeOffset.UtcNow.AddHours(3), // 1 hour before (meeting in 4 hrs)
DateTimeOffset.UtcNow.AddMinutes(225), // 15 min before
DateTimeOffset.UtcNow.AddHours(4) // Meeting time
};
for (int i = 0; i < messages.Count; i++)
{
long seqNum = await sender.ScheduleMessageAsync(messages[i], deliveryTimes[i]);
Console.WriteLine($"Scheduled '{messages[i].Body}' at {deliveryTimes[i]}. SeqNo: {seqNum}");
}
Scheduled Messages in Python SDK
from azure.servicebus import ServiceBusClient, ServiceBusMessage
from datetime import datetime, timezone, timedelta
connection_str = "Endpoint=sb://myshopns.servicebus.windows.net/;..."
queue_name = "orders"
with ServiceBusClient.from_connection_string(connection_str) as client:
with client.get_queue_sender(queue_name) as sender:
msg = ServiceBusMessage(
body="Process this at midnight",
message_id="midnight-order-101"
)
# Schedule 30 minutes from now
delivery_time = datetime.now(timezone.utc) + timedelta(minutes=30)
sequence_number = sender.schedule_messages(msg, delivery_time)
print(f"Scheduled. SequenceNumber: {sequence_number}")
# Cancel if needed
# sender.cancel_scheduled_messages(sequence_number)
Scheduled Message State Diagram
Sender calls ScheduleMessageAsync()
|
v
[ Scheduled State ] -- message invisible to consumers
|
| ScheduledEnqueueTime reached
v
[ Active State ] -- message visible to consumers
|
| Consumer reads and completes it
v
[ Deleted ] -- message removed
OR
[ Scheduled State ]
|
| CancelScheduledMessageAsync() called
v
[ Cancelled ] -- message removed without delivery
Important Considerations
| Consideration | Detail |
|---|---|
| Time Zone | Always use UTC for ScheduledEnqueueTime — Service Bus operates in UTC |
| Max Scheduled Messages | Up to 100 million scheduled messages per namespace (Standard/Premium) |
| Message TTL | TTL starts counting from when the message enters the queue, not from the scheduled time |
| Cancellation Window | Cancel before the scheduled time — cannot cancel an already-active message |
| Sequence Number Storage | Store SequenceNumber in a database if cancellation may be needed later |
TTL and Scheduled Time — Warning
Message TTL = 10 minutes Scheduled at = UtcNow + 15 minutes TTL starts from the time the message enters the queue. At 10 minutes, the message expires BEFORE the scheduled delivery time (15 min). The message is LOST or moves to DLQ. FIX: Always set TTL greater than ScheduledEnqueueTime offset. TTL = 20 minutes (safe margin above 15-minute schedule)
Best Practices for Scheduled Messages
- Always store the SequenceNumber in a database if cancellation is a business requirement
- Set TimeToLive well above the scheduled delivery offset to avoid expiry before delivery
- Use UTC timestamps — avoid local time zones to prevent off-by-one-hour errors during daylight saving changes
- Use ScheduleMessagesAsync (batch) when scheduling large volumes at once
- Set meaningful MessageId values — this helps with duplicate detection if the same schedule is submitted twice
Summary
Scheduled messages in Azure Service Bus allow publishers to store a message immediately but delay its visibility to consumers until a specified future time. Setting ScheduledEnqueueTime on the message is the simplest approach. Using ScheduleMessageAsync() returns a SequenceNumber that enables cancellation before the delivery time. Scheduled messages are invisible until their scheduled time and then become active and visible to consumers. Time zone handling and TTL configuration require careful attention to avoid silent message loss.
