SNAT - Azure App Service socket exhaustion
Did you know that using HttpClient
within a using
statement can cause SNAT (Source Network Address Translation) port exhaustion? This can lead to serious performance issues in your application.
What happens when SNAT ports are exhausted?
- Slow or hanging connections to remote endpoints
- Socket exceptions due to connection timeouts
This issue is hard to replicate in a local environment, but it can severely impact production systems, causing widespread disruptions.
Why is this important?
If your application interacts with multiple third-party APIs, a misconfigured HttpClient
can slow down or cause timeouts across all your HTTP requests, even those that are unrelated.
I've seen many different ways to use HttpClient
in a wrong way. Few examples:
public async Task<HttpResponseMessage> Get(string? requestUri)
{
return await new HttpClient().GetAsync(requestUri);
}
In C#, the using
statement is typically used to ensure that disposable objects, like HttpClient
, are properly disposed of once the block is exited.
However, in the case of HttpClient
, it’s best to avoid this pattern. While HttpClient
does implement the IDisposable
interface, it is designed to be a shared object, meaning that it should not be frequently disposed of and recreated.
public async Task<HttpResponseMessage> Get(string? requestUri)
{
using (var httpClient = new HttpClient())
return await httpClient.GetAsync(requestUri);
}
[ServiceConfiguration(typeof(IMyService), Lifecycle = ServiceInstanceScope.Scoped)]
public class MyService : IMyService
{
private readonly HttpClient _httpClient = new HttpClient();
public async Task<HttpResponseMessage> Get(string? requestUri)
{
return await _httpClient.GetAsync(requestUri);
}
}
Instead of creating a new instance of HttpClient for each execution you should share a single instance of HttpClient for the entire lifetime of the application.
Here i'll show a better way to do so:
[ServiceConfiguration(typeof(IMyService), Lifecycle = ServiceInstanceScope.Singleton)] will help you to have a singleton for IMyService instance
[ServiceConfiguration(typeof(IMyService), Lifecycle = ServiceInstanceScope.Singleton)]
public class MyService : IMyService
{
private readonly HttpClient _httpClient = new HttpClient();
public async Task<HttpResponseMessage> Get(string? requestUri)
{
return await _httpClient.GetAsync(requestUri);
}
}
In .NET Core, it's recommended to create a reusable client using an injected IHttpClientFactory
object.
Now i'll show the best way to use IHttpClientFactory
with services:
// register specific client in StartUp.cs
services.AddHttpClient("MyClient", (provider, client) => {
client.BaseAddress = new Uri("YOU URL");
client.DefaultRequestHeaders.Add("x-api-key", "YOUR API KEY");
});
[ServiceConfiguration(typeof(IMyService), Lifecycle = ServiceInstanceScope.Singleton)]
public class MyService : IMyService
{
private readonly IHttpClientFactory _httpClientFactory;
public OrderHistorySyncService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HttpResponseMessage> Get(string? requestUri)
{
return await _httpClientFactory.CreateClient("MyClient").GetAsync(requestUri);
}
}
Good job Olek, that explains a lot!