Posted on Sep 1 AI-assisted
Hey there, fellow C# developers! π
Let me tell you a horror story. You’re building a nice web service, everything works great in development, and then… production hits. Suddenly your app is throwing SocketException errors left and right. What happened?
You probably did this:
// DON'T DO THIS!
using (var client = new HttpClient())
{
var response = await client.GetAsync("https://api.example.com/data");
// ...
}
Enter fullscreen mode Exit fullscreen mode
The Socket Exhaustion Problem
Here’s the thing: HttpClient implements IDisposable, so your instincts tell you to wrap it in a using statement. Makes sense, right? Wrong!
When you dispose of an HttpClient, the underlying socket doesn’t close immediately. It enters a TIME_WAIT state for about 240 seconds (4 minutes!). Under load, you’ll exhaust all available sockets before the old ones recycle.
π€― Fun Fact: A single Windows machine has roughly 16,000 ephemeral ports available by default. At 100 requests/second with disposed clients, you’ll hit socket exhaustion in under 3 minutes!
Enter HttpClientFactory
.NET Core 2.1 introduced IHttpClientFactory to solve this elegantly. It pools and reuses HttpMessageHandler instances while still allowing proper DNS rotation.
Basic Setup
// In Program.cs or Startup.cs
services.AddHttpClient();
Enter fullscreen mode Exit fullscreen mode
// In your service
public class MyService
{
private readonly IHttpClientFactory _factory;
public MyService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task<string> GetDataAsync()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("https://api.example.com/data");
return await response.Content.ReadAsStringAsync();
}
}
Enter fullscreen mode Exit fullscreen mode
Named Clients
Want pre-configured clients for different APIs? Use named clients:
services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
});
services.AddHttpClient("weather", client =>
{
client.BaseAddress = new Uri("https://api.weather.com/");
});
Enter fullscreen mode Exit fullscreen mode
Then retrieve by name:
var githubClient = _factory.CreateClient("github");
var weatherClient = _factory.CreateClient("weather");
Enter fullscreen mode Exit fullscreen mode
Typed Clients (My Favorite!)
The cleanest approach β inject a fully configured HttpClient directly into your typed service:
services.AddHttpClient<IGitHubClient, GitHubClient>(client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
Enter fullscreen mode Exit fullscreen mode
public class GitHubClient : IGitHubClient
{
private readonly HttpClient _client;
// HttpClient is injected, pre-configured!
public GitHubClient(HttpClient client)
{
_client = client;
}
public async Task<User> GetUserAsync(string username)
{
var response = await _client.GetAsync($"users/{username}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<User>();
}
}
Enter fullscreen mode Exit fullscreen mode
π‘ Pro Tip: Handler Lifetime
By default, handlers are recycled every 2 minutes to handle DNS changes. You can customize this:
services.AddHttpClient("long-lived")
.SetHandlerLifetime(TimeSpan.FromMinutes(10));
Enter fullscreen mode Exit fullscreen mode
But be careful with long lifetimes β if your target service’s IP changes (common with cloud load balancers), you’ll keep hitting the old address!
Wrapping Up
Stop creating HttpClient instances manually. Use IHttpClientFactory and let .NET manage the connection pooling for you. Your ops team will thank you when socket exhaustion errors disappear from your logs! π
Happy coding! π