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

TypeAvailable to ConsumerStored Immediately
Regular MessageImmediatelyYes
Scheduled MessageOnly at the ScheduledEnqueueTimeYes (but invisible)

Real-World Use Cases

Use CaseExample
Reminder NotificationsSend an email reminder 24 hours after a cart abandonment
Delayed Order ProcessingProcess an order at 9 AM the next business day
Subscription Renewal AlertsSend a renewal reminder 7 days before expiry
Rate Limiting / ThrottlingSchedule a retry message 5 minutes from now after a failure
Campaign SchedulingTrigger a promotional email at 8 AM on a campaign launch date
Delayed CancellationCancel 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

ConsiderationDetail
Time ZoneAlways use UTC for ScheduledEnqueueTime — Service Bus operates in UTC
Max Scheduled MessagesUp to 100 million scheduled messages per namespace (Standard/Premium)
Message TTLTTL starts counting from when the message enters the queue, not from the scheduled time
Cancellation WindowCancel before the scheduled time — cannot cancel an already-active message
Sequence Number StorageStore 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.

Leave a Comment