Service Bus Message Sessions

Azure Service Bus Message Sessions provide a way to group related messages together and guarantee they are processed in order by a single consumer. Without sessions, multiple consumers process messages in parallel, and the order is not guaranteed. Sessions solve this problem by assigning all messages sharing the same SessionId to one consumer at a time.

Why Sessions Are Needed

Consider an order fulfillment workflow. An order goes through steps: Created → Payment Confirmed → Packed → Shipped. Each step produces a message. Without sessions, multiple consumers pick up messages in random order. Payment Confirmed might be processed before Created. Sessions ensure all messages for one order go to the same consumer in the correct sequence.

Without Sessions — No Order Guarantee

Messages for Order-101: [Created] [Paid] [Packed] [Shipped]

Consumer A gets: [Paid]
Consumer B gets: [Created]
Consumer C gets: [Shipped]
Consumer A gets: [Packed]

Result: Messages processed OUT OF ORDER -- workflow broken!

With Sessions — Order Guaranteed Per Group

Messages for Order-101 (SessionId = "order-101"):
  [Created] [Paid] [Packed] [Shipped]

Session assigned to Consumer A:
  Consumer A processes: [Created] --> [Paid] --> [Packed] --> [Shipped]

IN ORDER, one consumer handles all messages for this session.

How Sessions Work Internally

Queue (Sessions Enabled)
  |
  |-- SessionId = "order-101": [msg1][msg2][msg3]
  |-- SessionId = "order-102": [msg4][msg5]
  |-- SessionId = "order-103": [msg6][msg7][msg8]

Consumer A accepts session "order-101" --> processes msg1, msg2, msg3 in order
Consumer B accepts session "order-102" --> processes msg4, msg5 in order
Consumer C accepts session "order-103" --> processes msg6, msg7, msg8 in order

Different sessions run in PARALLEL. Messages within ONE session run in SEQUENCE.

Enable Sessions on a Queue

Azure Portal

  1. Open the namespace and click Queues
  2. Click + Queue
  3. Check the Enable Sessions checkbox
  4. Click Create

Sessions cannot be enabled or disabled after queue creation. Create a new queue if the existing one needs sessions added.

Azure CLI

az servicebus queue create \
  --resource-group rg-messaging-prod \
  --namespace-name myshopns \
  --name order-session-queue \
  --enable-session true

Sending Session Messages — .NET

using Azure.Messaging.ServiceBus;

await using var client = new ServiceBusClient(connectionString);
await using var sender = client.CreateSender("order-session-queue");

// All messages for order-101 share the same SessionId
string sessionId = "order-101";

var steps = new[] { "Created", "PaymentConfirmed", "Packed", "Shipped" };

foreach (var step in steps)
{
    var message = new ServiceBusMessage(step)
    {
        SessionId = sessionId,
        MessageId = $"order-101-{step}"
    };
    await sender.SendMessageAsync(message);
    Console.WriteLine($"Sent: {step} for session {sessionId}");
}

Sending Messages for Multiple Sessions

// Simulate two orders (two sessions) sent interleaved
string[] orders = { "order-101", "order-102" };
string[] steps  = { "Created", "Paid", "Shipped" };

foreach (var step in steps)
{
    foreach (var orderId in orders)
    {
        var message = new ServiceBusMessage($"{orderId}: {step}")
        {
            SessionId = orderId
        };
        await sender.SendMessageAsync(message);
    }
}

// Queue now contains:
//   [order-101: Created] [order-102: Created]
//   [order-101: Paid]    [order-102: Paid]
//   [order-101: Shipped] [order-102: Shipped]

Receiving Session Messages — .NET

Accept a Specific Session

await using var sessionReceiver = await client.AcceptSessionAsync(
    "order-session-queue",
    sessionId: "order-101"
);

Console.WriteLine($"Session accepted: {sessionReceiver.SessionId}");

// Receive all messages in this session in order
while (true)
{
    var msg = await sessionReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(3));
    if (msg == null) break;

    Console.WriteLine($"Received: {msg.Body}");
    await sessionReceiver.CompleteMessageAsync(msg);
}

Console.WriteLine("All messages in session processed.");

Accept the Next Available Session (Dynamic)

// Let Service Bus assign any available session to this consumer
await using var sessionReceiver = await client.AcceptNextSessionAsync("order-session-queue");

Console.WriteLine($"Accepted session: {sessionReceiver.SessionId}");

ServiceBusReceivedMessage msg;
while ((msg = await sessionReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(3))) != null)
{
    Console.WriteLine($"Processing: {msg.Body}");
    await sessionReceiver.CompleteMessageAsync(msg);
}

Session Processor — Push-Based Receiving

var sessionProcessor = client.CreateSessionProcessor(
    "order-session-queue",
    new ServiceBusSessionProcessorOptions
    {
        MaxConcurrentSessions         = 3,  // Process 3 sessions simultaneously
        MaxConcurrentCallsPerSession  = 1   // One message at a time per session
    }
);

sessionProcessor.ProcessMessageAsync += async args =>
{
    Console.WriteLine($"Session: {args.Message.SessionId}");
    Console.WriteLine($"Message: {args.Message.Body}");
    await args.CompleteMessageAsync(args.Message);
};

sessionProcessor.ProcessErrorAsync += async args =>
{
    Console.WriteLine($"Session error: {args.Exception.Message}");
    await Task.CompletedTask;
};

await sessionProcessor.StartProcessingAsync();
Console.WriteLine("Session processor started. Press Enter to stop.");
Console.ReadLine();
await sessionProcessor.StopProcessingAsync();

Session Processor — Concurrency Diagram

MaxConcurrentSessions = 3

Session "order-101" --> Consumer Thread 1 --> [msg1][msg2][msg3] in sequence
Session "order-102" --> Consumer Thread 2 --> [msg4][msg5]       in sequence
Session "order-103" --> Consumer Thread 3 --> [msg6][msg7][msg8] in sequence

All three sessions run IN PARALLEL.
Messages within each session run IN ORDER.

Session State

Sessions support storing a small piece of session state — a byte array saved by the receiver and retrieved later. This is useful for tracking workflow progress within a session across multiple consumer calls.

// Save state for the session
byte[] stateData = System.Text.Encoding.UTF8.GetBytes("Step: PaymentConfirmed");
await sessionReceiver.SetSessionStateAsync(BinaryData.FromBytes(stateData));

// Retrieve state later
BinaryData savedState = await sessionReceiver.GetSessionStateAsync();
Console.WriteLine($"Session state: {savedState}");

Session State Use Case

Workflow: Order 101

Step 1 processed: State saved = "Created"
Step 2 processed: State saved = "PaymentConfirmed"

Consumer crashes during Step 3.

New consumer accepts session "order-101":
  Reads state: "PaymentConfirmed"
  Knows it must continue from Step 3 (Packed)
  No steps are repeated.

Sessions and PartitionKey

In partitioned queues, the PartitionKey must match the SessionId. This ensures all messages for one session go to the same partition and are processed in order by the partition's single consumer.

var message = new ServiceBusMessage("Order step data")
{
    SessionId    = "order-101",
    PartitionKey = "order-101"  // Must match SessionId in partitioned queues
};

Sessions Summary Table

FeatureWithout SessionsWith Sessions
Message OrderNot guaranteedGuaranteed per SessionId
Consumer AssignmentAny consumer gets any messageOne consumer owns all messages in a session
Parallel ProcessingAll messages in parallelSessions in parallel, messages within a session in sequence
Session StateNot supportedSupported — save workflow progress
Partitioned QueuePartitionKey optionalPartitionKey must equal SessionId

Summary

Azure Service Bus Message Sessions guarantee ordered, grouped processing of related messages. Setting a SessionId on related messages ensures they go to one consumer in the correct sequence. The SessionProcessor handles multiple sessions concurrently while preserving order within each session. Session state enables stateful workflows that survive consumer restarts. Sessions are ideal for multi-step workflows, financial transaction sequences, and any scenario where message order within a logical group is critical.

Leave a Comment