Managing HttpClient Lifetimes with IHttpClientFactory in .NET
Use IHttpClientFactory to pool HttpMessageHandler instances, avoid socket exhaustion, and respect DNS changes. Register named or typed clients in Program.cs, inject the factory or typed interface, and tune handler lifetime for your deployment environment.
03 Jan 2026, 04:33 UTC

Use IHttpClientFactory Instead of Manual HttpClient Creation
Creating a new HttpClient per request exhausts sockets under load because each instance holds a separate HttpMessageHandler and underlying connection pool. A single static HttpClient avoids socket exhaustion but caches DNS entries indefinitely, causing failures when backend IPs change. IHttpClientFactory solves both problems: it pools HttpMessageHandler instances, reuses connections, and recycles handlers after a configurable lifetime (default two minutes) so DNS changes are respected.
How the Factory Pools Handlers
When you register a named or typed client, the factory creates a HttpMessageHandler pipeline (including any DelegatingHandler middleware you add) and caches it. Each call to CreateClient or injection of a typed client returns a new HttpClient that wraps the same handler instance. The factory tracks handler age and disposes expired handlers in the background, replacing them with fresh ones on the next request. This design keeps connection reuse high while bounding DNS staleness.
Worked Example: Named Client Registration and Use
In an ASP.NET Core minimal API or MVC project targeting .NET 6 or later, register a named client in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("github", client =
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
// Optional: set a default timeout for all requests using this client
client.Timeout = TimeSpan.FromSeconds(30);
});
var app = builder.Build();
app.MapGet("/repos/{owner}/{repo}", async (string owner, string repo,
IHttpClientFactory factory, CancellationToken ct) =
{
var client = factory.CreateClient("github");
var response = await client.GetAsync($"repos/{owner}/{repo}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(ct);
});
app.Run();
Where to run: This code belongs in the application startup file (Program.cs for .NET 6+ minimal hosting). No elevated permissions are required; the factory is registered in the DI container like any other service.
Expected check: Start the app and call the endpoint multiple times. Add a custom DelegatingHandler that logs its constructor to verify the handler is created once and reused across requests.
Typed Clients for Cleaner Dependencies
Instead of injecting IHttpClientFactory and calling CreateClient, define an interface and implementation that receives a pre-configured HttpClient in its constructor:
public interface IGitHubClient
{
Task<Repository> GetRepositoryAsync(string owner, string repo, CancellationToken ct);
}
public class GitHubClient : IGitHubClient
{
private readonly HttpClient _http;
public GitHubClient(HttpClient http) => _http = http;
public async Task<Repository> GetRepositoryAsync(string owner, string repo, CancellationToken ct)
{
var response = await _http.GetAsync($"repos/{owner}/{repo}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Repository>(cancellationToken: ct);
}
}
// Registration in Program.cs
builder.Services.AddHttpClient<IGitHubClient, GitHubClient>(client =
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json");
});
Controllers or services then inject IGitHubClient directly. The factory creates the HttpClient once per handler lifetime and passes it to the GitHubClient constructor.
Limits: Timeouts, Retries, and Disposal
- No default per-request timeout. The
HttpClient.Timeoutproperty (set in the configuration lambda) applies to the entire request including redirects. For granular control, add aDelegatingHandlerthat wraps each request with aCancellationTokenSource. - No built-in retry. Transient faults (5xx, network blips) are not retried automatically. Integrate Polly via
AddPolicyHandlerorAddTransientHttpErrorPolicy(requiresMicrosoft.Extensions.Http.Pollypackage). - Do not dispose factory-created clients. Wrapping the returned
HttpClientin ausingblock or callingDispose()is harmless but defeats pooling because the factory still owns the handler. Let the factory manage handler disposal.
Common Mistakes to Avoid
| Mistake | Why It Hurts | Fix |
|---|---|---|
using var client = factory.CreateClient("name"); |
Disposing the client does not release the handler, but it signals intent to discard and can confuse readers. The handler stays pooled. | Use the client directly without using. |
Static HttpClient field |
Ignores DNS TTL changes; sockets accumulate under load because each static instance has its own connection pool. | Register with IHttpClientFactory. |
Creating new HttpClient() per request |
Each instance creates a new handler and connection pool, leading to port exhaustion (TIME_WAIT storms). | Inject IHttpClientFactory or a typed client. |
Forgetting SetHandlerLifetime when DNS rotates frequently |
Default two-minute lifetime may cache stale IPs in environments with short DNS TTLs (e.g., Kubernetes services, cloud load balancers). | Call .SetHandlerLifetime(TimeSpan.FromSeconds(30)) on the IHttpClientBuilder. |
Tuning Handler Lifetime
The default handler lifetime is two minutes. Reduce it when backend endpoints change IP addresses more often (e.g., containerized services with rolling deployments, external APIs with low DNS TTL). Increase it for stable on-premises endpoints to maximize connection reuse.
builder.Services.AddHttpClient("external-api", client =
{
client.BaseAddress = new Uri("https://api.example.com/");
})
.SetHandlerLifetime(TimeSpan.FromSeconds(30)); // recycle handler every 30 seconds
Risk: Setting lifetime too low increases handler churn, causing more TCP handshakes and TLS negotiations. Monitor connection metrics (see verification) before aggressively lowering the value.
Verifying Handler Reuse and Connection Health
- Add a diagnostic
DelegatingHandlerthat increments a static counter in its constructor and logs the value on each request. - Register it:
builder.Services.AddHttpClient("github").AddHttpMessageHandler<LoggingHandler>(); - Run concurrent requests (e.g.,
hey -n 100 -c 10 http://localhost:5000/repos/dotnet/runtime). - Confirm the constructor counter increments only once per handler lifetime (default two minutes), not per request.
- Compare socket usage:
netstat -an | findstr :443 | findstr ESTABLISHED | wc -l(Windows) orss -tan state established sport = :443 | wc -l(Linux) while load testing. Factory-backed clients should show far fewer concurrent connections thannew HttpClient()per request.
This verification works on .NET Core 2.1+, .NET 5+, and .NET 6+. The IHttpClientFactory abstractions are not available in .NET Framework 4.x.
Summary
IHttpClientFactory is the recommended way to create HttpClient instances in modern .NET. It pools handlers, recycles them to honor DNS changes, and integrates cleanly with dependency injection. Configure named or typed clients at startup, avoid disposing factory-created clients, and add explicit timeouts or Polly policies for resilience. Tune SetHandlerLifetime when your infrastructure demands faster DNS refresh.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.