Mastering .NET Core’s HttpClientFactory: Lifetime, DNS, and Resilience
Avoid socket exhaustion and DNS churn in .NET Core by using HttpClientFactory. Learn how named and typed clients share handlers, integrate Polly policies, and keep your services robust. Includes code, limits, and common pitfalls.
22 Apr 2026, 16:43 UTC

Why HttpClientFactory Is the Right Choice
In .NET Core, creating an HttpClient per request is a common anti‑pattern that leads to socket exhaustion, stale DNS entries, and thread‑safety issues. The IHttpClientFactory service, introduced in ASP.NET Core 2.1, solves these problems by managing HttpClient lifetimes and sharing underlying HttpMessageHandler instances. The result is a single, long‑lived handler that automatically refreshes DNS, while each call still receives a new HttpClient instance that is safe to use concurrently.
Mechanism Overview
The factory works in two layers:
- Handler reuse: A single
HttpMessageHandler(e.g.,SocketsHttpHandler) is created per named or typed client and kept alive for the application's lifetime. The handler handles DNS resolution and connection pooling. - Client creation: For each request, the factory returns a new
HttpClientthat points to the shared handler. This gives the performance of a singleton while preserving thread safety.
Because the handler is shared, DNS changes are automatically reflected after a configurable period (default 30 minutes). If you need more frequent updates, you can configure DnsRefreshTimeout on the handler.
Integrating Resilience with Polly
Polly is a .NET resilience library that offers retry, circuit‑breaker, timeout, and fallback policies. HttpClientFactory can wire these policies into each client at registration time, so you never have to add retry logic manually in your service code.
Concrete Example: A GitHub API Client
1. Register a Named Client with Policies
In Program.cs or Startup.cs, add the following to ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("DotNetCoreClient");
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
}
static IAsyncPolicy GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
static IAsyncPolicy GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(2, TimeSpan.FromMinutes(1));
}
Key points:
- Named client "GitHub" is identified by the string key.
- Policies are added via
AddPolicyHandlerand will be applied to every request made with this client. - The retry policy uses exponential back‑off; the circuit breaker trips after two consecutive failures.
2. Consume the Client in a Controller
Inject IHttpClientFactory and create the named client on demand:
[ApiController]
[Route("api/[controller]")]
public class GitHubController : ControllerBase
{
private readonly IHttpClientFactory _factory;
public GitHubController(IHttpClientFactory factory)
{
_factory = factory;
}
[HttpGet("user/{login}")]
public async Task GetUser(string login)
{
var client = _factory.CreateClient("GitHub");
var response = await client.GetAsync($"/users/{login}");
if (!response.IsSuccessStatusCode)
return StatusCode((int)response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
}
}
Notice that the controller only knows about IHttpClientFactory; it does not manage any HTTP details.
3. Verify Handler Reuse
To confirm that the handler is reused, you can enable logging of handler creation:
services.AddHttpClient("GitHub", client =>
{
// ...
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new SocketsHttpHandler();
// Log handler creation
Console.WriteLine("Created new SocketsHttpHandler instance");
return handler;
});
Run the API, hit the endpoint multiple times, and observe that the handler creation message appears only once. Subsequent requests reuse the same handler, proving that DNS updates are handled automatically by the handler’s internal cache.
Limits and Common Mistakes
1. Forgetting to Use the Factory
Creating new HttpClient() in a loop or per request kills the connection pool and causes the dreaded "socket exhaustion" error. Always obtain clients via IHttpClientFactory or a typed client.
2. Injecting HttpClient Directly into Controllers
Controllers should remain agnostic of HTTP concerns. Inject the factory or a typed client instead. Typed clients encapsulate configuration and policies in a dedicated service class.
3. Over‑configuring Handlers
Excessive customization of SocketsHttpHandler (e.g., disabling DNS caching) can defeat the benefits of the factory. Use the default handler unless you have a compelling reason.
4. Ignoring DNS Refresh Timing
The default DNS refresh timeout is 30 minutes. If you need more frequent updates (e.g., in a microservice that relies on short‑lived IPs), configure DnsRefreshTimeout on the handler:
services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new SocketsHttpHandler
{
DnsRefreshTimeout = TimeSpan.FromMinutes(5)
};
return handler;
});
5. Misusing Polly Policies
Polly policies are per client. If you need different policies for different endpoints, register separate named clients or use policy selectors.
Checklist Before Deployment
- All HTTP calls use
IHttpClientFactoryor typed clients. - Named clients are registered with required base addresses and headers.
- Resilience policies (retry, circuit breaker) are applied where appropriate.
- DNS refresh timeout is set to match your environment’s IP churn rate.
- Logging is enabled to confirm handler reuse.
By following these guidelines, you’ll eliminate socket exhaustion, keep DNS up‑to‑date, and add a robust layer of resilience to your .NET Core services.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.