
A few months ago, I shared my journey of using Azure Service Bus message sessions to handle a data integrity issue within our system. The shift from a non-session to a session-based queue was a critical step in ensuring our data was processed in a structured, sequential manner. While that solution worked, it introduced an unexpected performance bottleneck. This post is a continuation of my learning journey, highlighting how a simple default configuration, specifically SessionIdleTimeout
, can significantly impact your application’s throughput.
Unexpected Performance Bottleneck in Azure Service Bus
When we implemented our session-based solution, we set the MaxConcurrentSessions
in ServiceBusSessionProcessorOptions
to forty. After running for a few hours, we immediately noticed a critical performance issue: our consumer’s throughput was capped at only forty sessions per minute. This was a significant slowdown from what we were used to, and it caused events to pile up rapidly.
After extensive troubleshooting, we discovered the root cause was the default configuration in the .NET Azure SDK for the maximum time the processor waits for a message in an active session. This setting can be configured through SessionIdleTimeout
in ServiceBusSessionProcessorOptions
. If we don’t set SessionIdleTimeout
explicitly, the processor uses the client’s TryTimeout
. The default TryTimeout
in the Azure .NET SDK is 60 seconds, so the effective wait is 60 seconds unless we override it.
This meant our consumer could process up to forty concurrent sessions but could not accept new sessions until one of the existing ones timed out. This caused a massive backlog and created the very “queue up for a surprise” situation our title warns about.
Reproducing Azure Service Bus Session Bottleneck in .NET
To clearly demonstrate this behavior, we built two simple console applications. One is a sender and the other is a consumer. The sender continuously sends a stream of messages to a queue, ensuring each message belongs to unique sessions (e.g., session-id-1, session-id-2, etc.). As for the consumer, we set the MaxConcurrentSessions
to 8 sessions and leave out SessionIdleTimeout
so that processor will use the client’s TryTimeout
which default to 60 seconds. With these values, the consumer will process incoming messages for up to 8 separate sessions, and each of them will remain open to accept new messages for 60 seconds.
Sender Program.cs
// Azure Service Bus .NET example
static async Task Main(string[] args)
{
string connectionString = "<connection_string_to_azure_servicebus_namespace>";
string queueName = "session-idle-timeout-queue"; // replace this with your session enabled queue name
var client = new ServiceBusClient(connectionString);
var sender = client.CreateSender(queueName);
// Send 40 messages with unique (non-repeating) session IDs
for (int i = 0; i < 40; i++)
{
string sessionId = $"session-{i + 1:D4}"; // session-0001, session-0002, ...
string messageBody = $"Message {i + 1} sent on {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
var message = new ServiceBusMessage(messageBody)
{
SessionId = sessionId
};
await sender.SendMessageAsync(message);
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Sent: {messageBody} with SessionId: {sessionId}");
await Task.Delay(500);
}
await sender.DisposeAsync();
await client.DisposeAsync();
Console.WriteLine("All messages sent.");
}
Consumer Program.cs
// Azure Service Bus .NET example
static async Task Main(string[] args)
{
// Uncomment out this line to see the Azure SDK logs
//using var listener = new AzureEventSourceListener((e, message) =>
//{
// if (string.Equals(e.EventSource.Name, "Azure-Messaging-ServiceBus", StringComparison.Ordinal))
// {
// Console.WriteLine($"{DateTime.Now} {message}");
// }
//}, level: EventLevel.Verbose);
string connectionString = "<connection_string_to_azure_servicebus_namespace>";
string queueName = "session-idle-timeout-queue"; // replace this with your session enabled queue name
var client = new ServiceBusClient(connectionString);
var processorOptions = new ServiceBusSessionProcessorOptions
{
MaxConcurrentSessions = 8,
// For long-running handlers, prefer AutoCompleteMessages = false
// and call args.CompleteMessageAsync(...) after processing to avoid lock-expiry surprises.
AutoCompleteMessages = true
};
var processor = client.CreateSessionProcessor(queueName, processorOptions);
processor.ProcessMessageAsync += async args =>
{
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Received: Message from SessionId: {args.Message.SessionId}");
await Task.CompletedTask;
};
await processor.StartProcessingAsync();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
await processor.StopProcessingAsync();
await processor.DisposeAsync();
await client.DisposeAsync();
}
You will observe that it will accept a message from each of the eight sessions and then appear to “stop” processing any new messages. In the logs, you’ll see a delay of approximately 60 seconds before it can pick up new messages, as it waits for the SessionIdleTimeout
to expire on one of the open sessions. You will likely see the following pattern in the Azure Service Bus’s metrics where the outgoing messages is capped at the MaxConcurrentSessions
number.

Sender log shows that 40 messages are published in 22 seconds.
[2025-09-12 21:49:38] Sent: Message 1 sent on 2025-09-12 21:49:37 with SessionId: session-0001
[2025-09-12 21:49:39] Sent: Message 2 sent on 2025-09-12 21:49:39 with SessionId: session-0002
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
[2025-09-12 21:49:59] Sent: Message 39 sent on 2025-09-12 21:49:59 with SessionId: session-0039
[2025-09-12 21:50:00] Sent: Message 40 sent on 2025-09-12 21:50:00 with SessionId: session-0040
All messages sent.
Consumer log shows 60 seconds delay before processing the next 8 sessions.
[2025-09-12 21:49:38] Received: Message from SessionId: session-0001
[2025-09-12 21:49:39] Received: Message from SessionId: session-0002
[2025-09-12 21:49:39] Received: Message from SessionId: session-0003
[2025-09-12 21:49:40] Received: Message from SessionId: session-0004
[2025-09-12 21:49:40] Received: Message from SessionId: session-0005
[2025-09-12 21:49:41] Received: Message from SessionId: session-0006
[2025-09-12 21:49:42] Received: Message from SessionId: session-0007
[2025-09-12 21:49:42] Received: Message from SessionId: session-0008
----- 60 seconds delay between session-0001 and session-0015
[2025-09-12 21:50:39] Received: Message from SessionId: session-0015
[2025-09-12 21:50:39] Received: Message from SessionId: session-0009
[2025-09-12 21:50:40] Received: Message from SessionId: session-0010
[2025-09-12 21:50:40] Received: Message from SessionId: session-0025
[2025-09-12 21:50:41] Received: Message from SessionId: session-0022
[2025-09-12 21:50:41] Received: Message from SessionId: session-0026
[2025-09-12 21:50:42] Received: Message from SessionId: session-0030
[2025-09-12 21:50:42] Received: Message from SessionId: session-0011
----- 60 seconds delay between session-0015 and session-0012
[2025-09-12 21:51:39] Received: Message from SessionId: session-0012
[2025-09-12 21:51:39] Received: Message from SessionId: session-0019
[2025-09-12 21:51:40] Received: Message from SessionId: session-0033
[2025-09-12 21:51:40] Received: Message from SessionId: session-0023
[2025-09-12 21:51:41] Received: Message from SessionId: session-0018
[2025-09-12 21:51:42] Received: Message from SessionId: session-0016
[2025-09-12 21:51:42] Received: Message from SessionId: session-0029
[2025-09-12 21:51:43] Received: Message from SessionId: session-0017
[2025-09-12 21:52:39] Received: Message from SessionId: session-0035
....................................................................
Analysing Azure .NET SDK Logs for Session Bottleneck
Sadly, in production application log, the issue is not directly apparent because it clearly shows that our consumer processed incoming messages successfully, even though Azure metric highlighted the bottleneck in processing.
Hence, to troubleshot this issue further, we enabled logging in the Azure SDK to gather verbose logs from .NET Azure Service Bus SDK. Let’s do that with our test applications by uncommenting AzureEventSourceListener
code section and re-run both of the applications. This time the log will show additional information that can help us identify the likely root cause.
12/09/2025 11:23:26 pm Creating a ServiceBusClient (Namespace: 'codeygrove-learning-ns.servicebus.windows.net', Entity name: ''
12/09/2025 11:23:26 pm A ServiceBusClient has been created (Identifier 'codeygrove-learning-ns.servicebus.windows.net-e1912d7f-364b-4d6c-a4dc-90aca4a9b461').
12/09/2025 11:23:26 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: StartProcessingAsync start.
12/09/2025 11:23:26 pm Creating a ServiceBusSessionReceiver (Namespace: 'codeygrove-learning-ns.servicebus.windows.net', Entity name: 'session-idle-timeout-queue'
12/09/2025 11:23:26 pm Creating receive link for Identifier: session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733.
12/09/2025 11:23:26 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: StartProcessingAsync done.
Press any key to exit...
12/09/2025 11:23:26 pm session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733: Requesting authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/session-idle-timeout-queue
12/09/2025 11:23:27 pm session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733: Authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/session-idle-timeout-queue complete. Expiration time: 09/12/2025 11:53:26
12/09/2025 11:23:27 pm Receive link created for Identifier: session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733. Session Id: session-0001
12/09/2025 11:23:27 pm A ServiceBusSessionReceiver has been created (Identifier 'session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733').
12/09/2025 11:23:27 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: Processor RenewSessionLock start. SessionId = session-0001
12/09/2025 11:23:27 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: ReceiveBatchAsync start. MessageCount = 1
12/09/2025 11:23:27 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: ReceiveBatchAsync done. Received '1' messages. LockTokens = <LockToken>9ebd0c58-8556-43a9-9674-dc8f323e00fe</LockToken>
12/09/2025 11:23:27 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: User message handler start: Message: SequenceNumber: 71494644084506638, LockToken: 9ebd0c58-8556-43a9-9674-dc8f323e00fe
[2025-09-12 23:23:27] Received: Message from SessionId: session-0001
12/09/2025 11:23:27 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: User message handler complete: Message: SequenceNumber: 71494644084506638, LockToken: 9ebd0c58-8556-43a9-9674-dc8f323e00fe
12/09/2025 11:23:27 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: CompleteAsync start. MessageCount = 1, LockToken = 9ebd0c58-8556-43a9-9674-dc8f323e00fe
12/09/2025 11:23:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: CompleteAsync done. LockToken = 9ebd0c58-8556-43a9-9674-dc8f323e00fe
12/09/2025 11:23:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: ReceiveBatchAsync start. MessageCount = 1
12/09/2025 11:24:15 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: RenewSessionLockAsync start. SessionId = session-0001
12/09/2025 11:24:15 pm Creating management link for Identifier: session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733.
12/09/2025 11:24:15 pm session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733: Requesting authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/
12/09/2025 11:24:15 pm session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733: Authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/ complete. Expiration time: 09/12/2025 11:53:26
12/09/2025 11:24:16 pm Management link created for Identifier: session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733.
12/09/2025 11:24:16 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: RenewSessionLockAsync done. SessionId = session-0001
12/09/2025 11:24:16 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: Processor RenewSessionLock complete. SessionId = session-0001
12/09/2025 11:24:16 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: Processor RenewSessionLock start. SessionId = session-0001
12/09/2025 11:24:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001: ReceiveBatchAsync done. Received '0' messages. LockTokens =
12/09/2025 11:24:28 pm Closing a ServiceBusSessionReceiver (Identifier 'session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001').
12/09/2025 11:24:28 pm Management Link Closed. Identifier: session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733, linkException: .
12/09/2025 11:24:28 pm A ServiceBusSessionReceiver has been closed (Identifier 'session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0001').
12/09/2025 11:24:28 pm Creating a ServiceBusSessionReceiver (Namespace: 'codeygrove-learning-ns.servicebus.windows.net', Entity name: 'session-idle-timeout-queue'
12/09/2025 11:24:28 pm Creating receive link for Identifier: session-idle-timeout-queue-ba8061dc-9657-405c-a928-4068c95dead5.
12/09/2025 11:24:28 pm session-idle-timeout-queue-ba8061dc-9657-405c-a928-4068c95dead5: Requesting authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/session-idle-timeout-queue
12/09/2025 11:24:28 pm Receive Link Closed. Identifier: session-idle-timeout-queue-2bd12f6e-c73a-4872-a6f3-da69d3e22733, SessionId: session-0001, linkException: .
12/09/2025 11:24:28 pm session-idle-timeout-queue-ba8061dc-9657-405c-a928-4068c95dead5: Authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/session-idle-timeout-queue complete. Expiration time: 09/12/2025 11:53:26
12/09/2025 11:24:28 pm Receive link created for Identifier: session-idle-timeout-queue-ba8061dc-9657-405c-a928-4068c95dead5. Session Id: session-0002
12/09/2025 11:24:28 pm A ServiceBusSessionReceiver has been created (Identifier 'session-idle-timeout-queue-ba8061dc-9657-405c-a928-4068c95dead5').
12/09/2025 11:24:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: Processor RenewSessionLock start. SessionId = session-0002
12/09/2025 11:24:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0002: ReceiveBatchAsync start. MessageCount = 1
12/09/2025 11:24:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e-Ssession-0002: ReceiveBatchAsync done. Received '1' messages. LockTokens = <LockToken>e343df49-2c51-4b7a-8c68-8e5d279400b7</LockToken>
12/09/2025 11:24:28 pm session-idle-timeout-queue-029c235d-69f6-43f1-b9e1-c158ae5fa14e: User message handler start: Message: SequenceNumber: 51509920738050074, LockToken: e343df49-2c51-4b7a-8c68-8e5d279400b7
[2025-09-12 23:24:28] Received: Message from SessionId: session-0002
From the verbose log above, we can easily correlate our application’s log and Azure SDK’s logs and immediately observe that:
- Once the service bus receiver is created, it opened a session and successfully complete processing a message. Below is the simplified version of the above log for readability.
12/09/2025 11:23:26 pm Requesting authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/
12/09/2025 11:23:27 pm Authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/ complete.
12/09/2025 11:23:27 pm Receive link created for Session Id: session-0001
12/09/2025 11:23:27 pm A ServiceBusSessionReceiver has been created
12/09/2025 11:23:27 pm Processor RenewSessionLock start. SessionId = session-0001
12/09/2025 11:23:27 pm session-0001: ReceiveBatchAsync start. MessageCount = 1
12/09/2025 11:23:27 pm session-0001: ReceiveBatchAsync done. Received '1' messages.
12/09/2025 11:23:27 pm User message handler start: Message: SequenceNumber: 71494644084506638
[2025-09-12 23:23:27] Received: Message from SessionId: session-0001 (Application Log)
12/09/2025 11:23:27 pm User message handler complete: Message: SequenceNumber: 71494644084506638
12/09/2025 11:23:27 pm session-0001: CompleteAsync start. MessageCount = 1
12/09/2025 11:23:28 pm session-0001: CompleteAsync done.
12/09/2025 11:23:28 pm session-0001: ReceiveBatchAsync start. MessageCount = 1
- The last
ReceiveBatchAsync start
in the log indicates that the receiver is waiting for next incoming message and keep the session open. - When it is close to 60 seconds mark, the receiver will renew the session lock to keep the session alive. This is important in the scenario where the consumer takes longer than 60 seconds to process a message.
12/09/2025 11:23:27 pm CompleteAsync start. MessageCount = 1
12/09/2025 11:23:28 pm session-0001: CompleteAsync done.
12/09/2025 11:23:28 pm session-0001: ReceiveBatchAsync start. MessageCount = 1
12/09/2025 11:24:15 pm session-0001: RenewSessionLockAsync start. SessionId = session-0001
12/09/2025 11:24:15 pm Creating management link
12/09/2025 11:24:15 pm Requesting authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/
12/09/2025 11:24:15 pm Authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/ complete.
12/09/2025 11:24:16 pm Management link created
12/09/2025 11:24:16 pm session-0001: RenewSessionLockAsync done. SessionId = session-0001
12/09/2025 11:24:16 pm Processor RenewSessionLock complete. SessionId = session-0001
12/09/2025 11:24:16 pm Processor RenewSessionLock start. SessionId = session-0001
- And finally, in the log below,
ReceiveBatchAsync
returns 0 messages, which leads the processor to close that session’s receiver. Because the processor’sSessionIdleTimeout
is defaulted to useTryTimeout
value of 60 seconds, the effect is that the session’s receiver remains tied up for ~60 seconds before closing and allowing the next session to be accepted.
12/09/2025 11:24:16 pm Processor RenewSessionLock complete. SessionId = session-0001
12/09/2025 11:24:16 pm Processor RenewSessionLock start. SessionId = session-0001
12/09/2025 11:24:28 pm session-0001: ReceiveBatchAsync done. Received '0' messages.
12/09/2025 11:24:28 pm Closing a ServiceBusSessionReceiver (Identifier 'session-0001').
12/09/2025 11:24:28 pm Management Link Closed.
12/09/2025 11:24:28 pm A ServiceBusSessionReceiver has been closed (Identifier 'session-0001').
12/09/2025 11:24:28 pm Creating a ServiceBusSessionReceiver
12/09/2025 11:24:28 pm Creating receive link
12/09/2025 11:24:28 pm Requesting authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/
12/09/2025 11:24:28 pm Receive Link Closed. SessionId: session-0001
12/09/2025 11:24:28 pm Authorization to amqps://codeygrove-learning-ns.servicebus.windows.net/ complete.
12/09/2025 11:24:28 pm Receive link created for Session Id: session-0002
12/09/2025 11:24:28 pm A ServiceBusSessionReceiver has been created
12/09/2025 11:24:28 pm Processor RenewSessionLock start. SessionId = session-0002
12/09/2025 11:24:28 pm session-0002: ReceiveBatchAsync start. MessageCount = 1
12/09/2025 11:24:28 pm session-0002: ReceiveBatchAsync done. Received '1' messages.
12/09/2025 11:24:28 pm User message handler start: Message: SequenceNumber: 51509920738050074
[2025-09-12 23:24:28] Received: Message from SessionId: session-0002
Fixing Throughput by Adjusting SessionIdleTimeout in .NET
Once we correlated our application logs with the Azure SDK logs, the root cause became clear: the SessionIdleTimeout
was holding sessions open unnecessarily.
Resolving this issue is pretty straightforward, all we need to do is to override the SessionIdleTimeout
property in ServiceBusSessionProcessorOptions
. It is important to note that setting SessionIdleTimeout
property to low duration can have some implications. For instance, a higher CPU and allocation overhead because of frequent task scheduling, object creation and session lock handling. Or extra AMQP link operations which resulted in higher network chatter, possible throttling, and higher resource usage on broker and client.
Given these implications, it is important to set SessionIdleTimeout
based on your publisher’s behaviour. We might need to consider if the publisher will publish two or more messages under one session within few seconds? For our production system, since the publisher could publish two messages for a single session within two seconds, we overrode the SessionIdleTimeout
to 2 seconds.
// Refer to the consumer Program.cs above for full code
var processorOptions = new ServiceBusSessionProcessorOptions
{
// Override the SessionIdleTimeout based on the behaviour of your publisher
SessionIdleTimeout = TimeSpan.FromSeconds(2),
AutoCompleteMessages = true
};
Rerun the test applications and the logs will immediately show a significant improvement in throughput, with the consumer continuously accepting and processing messages without the long delays.

This simple example proves that a small change in configuration can have a massive impact on your application’s performance.
Conclusion
This experience was a powerful reminder that while default settings are often a good starting point, they are not a one-size-fits-all solution. In our case, the default SessionIdleTimeout
of 60 seconds, created a significant bottleneck. By understanding our specific system requirements and adjusting the SessionIdleTimeout
accordingly, we could optimise message processing and maintain high throughput. The key takeaway is to always investigate and fine-tune your configurations to align with your application’s unique needs. Don’t let default settings unexpectedly queue you up for a surprise!
Leave a Reply