Solving the HttpClient Lifecycle Dilemma in .NET
Stop choosing between socket exhaustion and stale DNS. Learn how IHttpClientFactory manages the HttpClient lifecycle in .NET to ensure high-performance, resilient API integrations.
23 Apr 2026, 13:32 UTC

The Socket Exhaustion vs. DNS Stale-Date Trap
When building .NET applications that consume external APIs, the instinct is often to wrap HttpClient in a using statement. This seems correct because HttpClient implements IDisposable. However, disposing of the client doesn't immediately release the underlying socket; it stays in a TIME_WAIT state. Under high load, your server will run out of available sockets, leading to System.Net.Sockets.SocketException errors.
The common fix is to make HttpClient a static or singleton instance. While this stops socket exhaustion, it introduces a new problem: DNS changes are ignored. If your API provider rotates IP addresses or fails over to a new region, your singleton client will keep trying to hit the old, dead IP until the application restarts.
The IHttpClientFactory pattern solves this by decoupling the HttpClient (the lightweight wrapper) from the HttpMessageHandler (the component that actually manages the connection). The factory manages a pool of handlers, rotating them periodically to ensure DNS records are refreshed while keeping sockets open for reuse.
Choosing Your Implementation Strategy
Depending on the complexity of your external integrations, you can implement the factory in three primary ways:
- Basic Client: A generic client used for one-off requests.
- Named Clients: Pre-configured clients identified by a string key. This is useful when you have multiple external services but don't want to create a class for each.
- Typed Clients: A custom class that accepts
HttpClientin its constructor. This is the gold standard for maintainability, as it encapsulates the API logic and allows for easy mocking during unit tests.
Implementation: The Typed Client Pattern
For most production scenarios, Typed Clients provide the best balance of type safety and configuration. This example assumes you are using .NET 6+ and the Microsoft.Extensions.Http NuGet package.
1. Define the Service Wrapper
public class GitHubService
{
private readonly HttpClient _httpClient;
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetRepoInfoAsync(string repo)
{
// The BaseAddress and Headers are already configured in Program.cs
return await _httpClient.GetStringAsync($"repos/{repo}");
}
}
2. Register in Dependency Injection
Run these configurations in your Program.cs or Startup.cs. This registration tells the DI container to provide an HttpClient specifically configured for the GitHubService.
builder.Services.AddHttpClient<GitHubService>(
client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("User-Agent", "ReadMeFeed-Technical-Blog");
client.Timeout = TimeSpan.FromSeconds(30);
});
3. Use the Service in a Controller or Minimal API
app.MapGet("/github/{repo}", async (string repo, GitHubService gitHubService) =>
{
return await gitHubService.GetRepoInfoAsync(repo);
});
Trade-offs and Lifecycle Risks
While IHttpClientFactory handles the heavy lifting, there are two critical pitfalls to avoid:
- Manual Disposal: Never call
.Dispose()on anHttpClientprovided by the factory. The factory tracks the handler's lifetime; disposing of the client manually can interfere with the pooling mechanism. - Handler Lifetime: By default, handlers are rotated every two minutes. If your environment has extremely aggressive DNS TTL (Time to Live) settings, you may need to adjust the
HandlerLifetime. Setting this too high increases the risk of stale DNS; setting it too low increases the overhead of creating new connections.
Verifying the Result
To verify that your implementation is preventing socket exhaustion, you can monitor the socket states on your machine during a load test. Run the following command in a terminal (Windows) while your application is under load:
netstat -an | findstr TIME_WAIT
If you see thousands of connections in TIME_WAIT, you are likely still instantiating HttpClient manually. With IHttpClientFactory, you should see a stable, significantly lower number of connections regardless of request volume.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.