Solving Port Exhaustion Issue In .NET

When developing applications that connect to an external resource such as databases, queues, or storage systems, we use classes that make these connections by abstracting away the underlying implementation.
In this article, we will look at how to properly use such classes to avoid port exhaustion.
Let's get started!
Port Exhaustion Issue
When we access external resources over TCP or UDP, we do so over a port. And while there are 65,535 ports available, our system can quickly run out of ports to use if we improperly instantiate classes that use these ports.
A good example of such a class in .NET is the HttpClient
class. Other examples include classes in the Azure SDK such as:
ServiceBus.Messaging.QueueClient
for Azure Service BusAzure.Documents.Client.DocumentClient
for Azure Cosmos DBStackExchange.Redis.ConnectionMultiplexer
for Redis CacheAzure.Storage
.Blobs
for Azure Storage Blob
These classes ought to be instantiated once and used for the lifetime of the application.
When we use HttpClient
to request a URL:
public class HttpService
{
public HttpService() { }
public async Task<string> GetDataAsync()
{
string url = "https://www.google.com";
using var _client = new HttpClient();
var response = await _client.GetStringAsync(url);
Console.WriteLine(response);
return response!;
}
}
Here, we can see that we instantiate a new HttpClient
each time we want to make a request to the URL. While this may work fine for an application that is run once, this could lead to socket exceptions when we have an application that processes a lot of requests e.g. a web application.
When this happens, our application throws a SocketException
error:
"An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full"
Diagnosing Port Exhaustion Issues
Port exhaustion issues can lead to SocketException
errors and increased resource usage which will make our applications unresponsive.
One quick way of diagnosing the problem is to run the netstat
command:
> netstat
This would produce a list of running processes accessing the ports and we can quickly see which process has opened a lot of ports:
Properly Instantiating Classes
To solve this, we must instantiate one instance of the class if the class is thread-safe. This can be achieved by creating a shared singleton instance or a pool of instances.
We can refactor our HttpService
to use a single instance of HttpClient
:
public class HttpService
{
private static HttpClient? _client;
public HttpService()
{
_client = new HttpClient();
}
public async Task<string> GetDataAsync()
{
string url = "https://www.google.com";
var response = await _client!.GetStringAsync(url);
Console.WriteLine(response);
return response!;
}
}
Here, we can see that we declare a static
instance of the HttpClient
and initialize it once. This ensures that only one instance of the HttpClient
is used throughout the lifetime of the application.
If our application is using Dependency Injection, we can inject the HttpClient
by registering it during startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
}
We can then access the HttpClient
in our code:
public class HttpService
{
private readonly IHttpClientFactory _clientFactory;
public HttpService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetDataAsync()
{
string url = "https://www.google.com";
HttpClient _client = _clientFactory.CreateClient();
var response = await _client!.GetStringAsync(url);
Console.WriteLine(response);
return response!;
}
}
Properly Instantiating Azure Services
Let's look at how to instantiate some Azure services in a way that doesn't lead to part exhaustion.
If our application uses Dependency Injection, we could easily use the Azure extension method to register our services and let the Inversion of Control library manage the pool of resources:
public void ConfigureServices(IServiceCollection services)
{
services.AddAzureClients(builder =>
{
builder.AddBlobServiceClient("ConnectionString");
builder.AddServiceBusClient("ConnectionString");
});
}
Here, we use the Azure SDK extension to register our Azure Blob and Azure Service Bus services.
We can then use the services in our code:
public class ServiceClass
{
private readonly ServiceBusClient _client;
private readonly BlobServiceClient _blobServiceClient;
public ServiceClass(ServiceBusClient client, BlobServiceClient _blobServiceClient)
{
_client = client;
_blobServiceClient = blobServiceClient;
}
}
For Azure CosmosDb, we create a singleton that can be reused across our application. We do this because the CosmosClient
is thread-safe and one instance can be shared:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<CosmosClient>(provider =>
{
CosmosClientOptions cosmosClientOptions = new CosmosClientOptions()
{
ConnectionMode = ConnectionMode.Direct
};
var client = new CosmosClient("Connection string", "Authentication Key", cosmosClientOptions);
return client;
}
}
Conclusion
Properly instantiating classes that use the network helps us avoid issues within our application. In this article, we learned how to avoid instantiating these classes in a way that will not lead to port exhaustion and other issues in our application.
Useful links:
Subscribe to my newsletter
Read articles from Bayo A directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Bayo A
Bayo A
Software engineer with a passion for learning and trying out new tech.