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");
// ...
}
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();
// 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();
}
}
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/");
});
Then retrieve by name:
var githubClient = _factory.CreateClient("github");
var weatherClient = _factory.CreateClient("weather");
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");
});
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>();
}
}
💡 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));
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! 🚀
Top comments (0)