When working with Microsoft Azure Service Bus, you may encounter the following exception:
“Cannot allocate more handles. The maximum number of handles is 4999.”
This issue typically arises due to improper dependency injection scope configuration for the ServiceBusClient
. In most cases, the ServiceBusClient
is registered as Scoped
instead of Singleton
, leading to the creation of multiple instances during the application lifetime, which exhausts the available handles.
In this blog post, we’ll explore the root cause and demonstrate how to fix this issue by using proper dependency injection in .NET applications.
Understanding the Problem
Scoped vs. Singleton
- Scoped: A new instance of the service is created per request.
- Singleton: A single instance of the service is shared across the entire application lifetime.
The ServiceBusClient
is designed to be a heavyweight object that maintains connections and manages resources efficiently. Hence, it should be registered as a Singleton to avoid excessive resource allocation and ensure optimal performance.
Before Fix: Using Scoped Registration
Here’s an example of the problematic configuration:
public void ConfigureServices(IServiceCollection services) { services.AddScoped(serviceProvider => { string connectionString = Configuration.GetConnectionString("ServiceBus"); return new ServiceBusClient(connectionString); }); services.AddScoped<IMessageProcessor, MessageProcessor>(); }
In this configuration:
- A new instance of
ServiceBusClient
is created for each HTTP request or scoped context. - This quickly leads to resource exhaustion, causing the “Cannot allocate more handles” error.
Solution: Switching to Singleton
To fix this, register the ServiceBusClient
as a Singleton, ensuring a single instance is shared across the application lifetime:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton(serviceProvider => { string connectionString = Configuration.GetConnectionString("ServiceBus"); return new ServiceBusClient(connectionString); }); services.AddScoped<IMessageProcessor, MessageProcessor>(); }
In this configuration:
- A single instance of
ServiceBusClient
is created and reused for all requests. - Resource usage is optimized, and the exception is avoided.
Sample Code: Before and After
Before Fix (Scoped Registration)
public interface IMessageProcessor { Task ProcessMessageAsync(); } public class MessageProcessor : IMessageProcessor { private readonly ServiceBusClient _client; public MessageProcessor(ServiceBusClient client) { _client = client; } public async Task ProcessMessageAsync() { ServiceBusReceiver receiver = _client.CreateReceiver("queue-name"); var message = await receiver.ReceiveMessageAsync(); Console.WriteLine($"Received message: {message.Body}"); await receiver.CompleteMessageAsync(message); } }
After Fix (Singleton Registration)
public void ConfigureServices(IServiceCollection services) { // Singleton registration for ServiceBusClient services.AddSingleton(serviceProvider => { string connectionString = Configuration.GetConnectionString("ServiceBus"); return new ServiceBusClient(connectionString); }); services.AddScoped<IMessageProcessor, MessageProcessor>(); } public class MessageProcessor : IMessageProcessor { private readonly ServiceBusClient _client; public MessageProcessor(ServiceBusClient client) { _client = client; } public async Task ProcessMessageAsync() { ServiceBusReceiver receiver = _client.CreateReceiver("queue-name"); var message = await receiver.ReceiveMessageAsync(); Console.WriteLine($"Received message: {message.Body}"); await receiver.CompleteMessageAsync(message); } }
Key Takeaways
- Always use Singleton scope for
ServiceBusClient
to optimize resource usage. - Avoid using Scoped or Transient scope for long-lived, resource-heavy objects.
- Test your application under load to ensure no resource leakage occurs.