Message Sessions — Guaranteed Ordering
Why Ordering Matters
In distributed systems, messages can arrive out of order due to competing consumers, network latency, or partitioning. For workflows like order processing, where events must follow a strict sequence (Created → Paid → Shipped → Delivered), out-of-order processing causes data corruption and logic errors.
What Are Sessions?
Sessions provide FIFO (First-In, First-Out) guarantee within a logical group of messages. Each session is identified by a SessionId property, and Service Bus ensures that messages sharing the same SessionId are delivered in the exact order they were enqueued — to a single consumer at a time.
┌─────────────────────────────────────────────────────────┐
│ Service Bus Queue │
│ (Sessions Enabled = true) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Session: A │ │ Session: B │ │ Session: C │ │
│ │ Msg1 → Msg2 │ │ Msg1 → Msg2 │ │ Msg1 → Msg2 │ │
│ │ → Msg3 │ │ → Msg3 │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼─────────┘
│ │ │
▼ ▼ ▼
Consumer 1 Consumer 2 Consumer 3
(locked to A) (locked to B) (locked to C)
Prerequisites
- Azure Service Bus Standard or Premium namespace
- .NET 8 SDK
- Azure CLI installed
Step 1: Enable Sessions on a Queue
Sessions must be enabled at queue creation time — they cannot be toggled on an existing queue.
RESOURCE_GROUP="rg-servicebus-tutorials"
NAMESPACE="sb-tutorials-ns"
QUEUE_NAME="orders-session-queue"
# Create a session-enabled queue
az servicebus queue create \
--resource-group $RESOURCE_GROUP \
--namespace-name $NAMESPACE \
--name $QUEUE_NAME \
--enable-session true \
--max-delivery-count 10

Step 2: Send Messages with SessionId
Every message sent to a session-enabled queue must include a SessionId. Messages without one will be rejected.
using Azure.Messaging.ServiceBus;
var connectionString = "<YOUR_CONNECTION_STRING>";
var queueName = "orders-session-queue";
await using var client = new ServiceBusClient(connectionString);
await using var sender = client.CreateSender(queueName);
// Simulate order lifecycle events for Order-1001
var orderId = "Order-1001";
var events = new[] { "Created", "Paid", "Shipped", "Delivered" };
foreach (var evt in events)
{
var message = new ServiceBusMessage($"{{\"orderId\":\"{orderId}\",\"event\":\"{evt}\"}}")
{
SessionId = orderId,
Subject = evt
};
await sender.SendMessageAsync(message);
Console.WriteLine($"Sent: {evt} for session {orderId}");
}
Step 3: Receive with Session Handler (ServiceBusSessionProcessor)
Use ServiceBusSessionProcessor to process messages in session order. The processor automatically locks a session and delivers messages sequentially.
using Azure.Messaging.ServiceBus;
var connectionString = "<YOUR_CONNECTION_STRING>";
var queueName = "orders-session-queue";
await using var client = new ServiceBusClient(connectionString);
var options = new ServiceBusSessionProcessorOptions
{
MaxConcurrentSessions = 5,
MaxConcurrentCallsPerSession = 1, // Ensures FIFO within session
AutoCompleteMessages = false,
SessionIdleTimeout = TimeSpan.FromSeconds(30)
};
await using var processor = client.CreateSessionProcessor(queueName, options);
processor.ProcessMessageAsync += async args =>
{
string sessionId = args.SessionId;
string body = args.Message.Body.ToString();
Console.WriteLine($"[Session: {sessionId}] Processing: {body}");
await args.CompleteMessageAsync(args.Message);
};
processor.ProcessErrorAsync += args =>
{
Console.WriteLine($"Error: {args.Exception.Message}");
return Task.CompletedTask;
};
await processor.StartProcessingAsync();
Console.WriteLine("Session processor started. Press any key to stop.");
Console.ReadKey();
await processor.StopProcessingAsync();

Step 4: Understand FIFO Guarantees
| Guarantee | Scope | Notes |
|---|---|---|
| Strict FIFO | Within a single session | Messages with same SessionId delivered in enqueue order |
| No ordering | Across sessions | Session A and Session B are independent |
| Exclusive lock | Per session | Only one consumer processes a session at a time |
| Session state | Per session | Store custom state (e.g., last processed sequence) |
Step 5: Use Session State for Checkpointing
processor.ProcessMessageAsync += async args =>
{
BinaryData state = await args.GetSessionStateAsync();
var lastProcessed = state != null ? state.ToString() : "none";
Console.WriteLine($"[{args.SessionId}] Last processed: {lastProcessed}");
string body = args.Message.Body.ToString();
// Process message...
await args.SetSessionStateAsync(BinaryData.FromString(args.Message.Subject));
await args.CompleteMessageAsync(args.Message);
};
Real-World Scenario: Order Lifecycle Events
┌──────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Order │ │ Service Bus │ │ Order Processor │
│ Service │────▶│ Session Queue │────▶│ (Session-aware) │
└──────────┘ └─────────────────┘ └──────────────────┘
│
SessionId = OrderId ▼
Events: Created → Paid ┌──────────────────┐
→ Shipped → Delivered │ Order Database │
│ (consistent state)│
└──────────────────┘
| Event | SessionId | Sequence | Action |
|---|---|---|---|
| Created | Order-1001 | 1 | Insert order record |
| Paid | Order-1001 | 2 | Update payment status |
| Shipped | Order-1001 | 3 | Record tracking number |
| Delivered | Order-1001 | 4 | Close order |
Key Takeaways
- Sessions guarantee FIFO per session, not globally
MaxConcurrentCallsPerSession = 1ensures sequential processing- Session state enables checkpointing and resumption
- Sessions must be enabled at queue creation time