Choosing the Right HttpClient Lifetime in .NET Core: Factory vs. Singleton vs. Per‑Call
Decide how long an HttpClient should live in .NET Core. Compare typed clients via IHttpClientFactory, singleton HttpClient with handler rotation, and per‑call creation. See a concise table, trade‑offs, and concrete code examples with validation steps.
23 Jan 2026, 14:57 UTC

Decision: Which HttpClient Lifetime to Use?
When building an ASP.NET Core or Generic Host application that calls external HTTP services, you must decide how long an HttpClient instance should live. The choice affects DNS resolution, connection reuse, socket exhaustion, and testability. This guide compares the three most common patterns—IHttpClientFactory typed/named clients, a singleton HttpClient backed by a SocketsHttpHandler, and per‑call HttpClient creation—and shows how to validate the selected approach.
Constraints & Decision Factors
- DNS Changes: Do you need the client to pick up DNS updates within a short window?
- Connection Reuse: Are you concerned about socket exhaustion under high load?
- Resilience Policies: Do you require retries, circuit breaking, or per‑request header manipulation?
- Testability: Will you need to inject a mock handler for integration tests?
- App Complexity: How many distinct HTTP endpoints do you target?
Options Overview
| Option | Lifetime | DNS Refresh | Connection Pooling | Resilience | Complexity | DI Pattern | Typical Use‑Case |
|---|---|---|---|---|---|---|---|
Typed/Named Clients via IHttpClientFactory | Handler pooled, rotated per PooledConnectionLifetime | Handler rotated; new DNS after rotation | Yes, pooled per handler | Polly or Microsoft.Extensions.Http.Resilience via AddPolicyHandler | Moderate (requires factory package) | Transient clients injected into services | Multiple external APIs with shared policies |
Singleton HttpClient with SocketsHttpHandler.PooledConnectionLifetime | Single handler, lifetime controlled manually | After PooledConnectionLifetime expires | Yes, but no automatic rotation | Manual via DelegatingHandler | Low (simple registration) | Singleton services or static helpers | Few endpoints, minimal policy needs |
Per‑Call HttpClient Creation | Disposed after each request | Immediate DNS resolution per call | No reuse; each call creates new socket | None unless wrapped in custom code | High (creates many objects) | Transient or scoped services that create clients on demand | Legacy code or one‑off requests |
Trade‑offs
- Typed Clients give you constructor injection, strong typing, and a central place to configure base address and default headers. They do not pool the
HttpClientitself—you rely on the factory’s handler pool. They are ideal when you need per‑client policies or want to unit‑test a service that depends on anHttpClient. - Singleton HttpClient is the simplest pattern but places the burden of retry, timeout, and header management on you. It also risks mutating shared
DefaultRequestHeaders, which is not thread‑safe. This pattern is suitable for small, low‑traffic services with minimal resilience requirements. - Per‑Call Creation guarantees fresh DNS and TLS handshakes but quickly exhausts the OS’s ephemerals under load. It is rarely recommended in production but can be useful for quick scripts or when you cannot use DI.
Concrete Implementation – Typed Client with Lifetime Control
Assume a service calls https://api.example.com. Install the package Microsoft.Extensions.Http (included by default in ASP.NET Core 2.1+).
// Program.cs or Startup.cs
services.AddHttpClient<ExampleApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
// Rotate the underlying handler every 30 minutes to pick up DNS changes
.SetHandlerLifetime(TimeSpan.FromMinutes(30))
// Add a retry policy (Polly) – optional
.AddPolicyHandler(Policy
.HandleResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2) }));
Inject ExampleApiClient into any transient or scoped service:
public class OrderService
{
private readonly ExampleApiClient _api;
public OrderService(ExampleApiClient api) => _api = api;
public async Task<Order> GetOrderAsync(int id)
{
var response = await _api.GetAsync($"orders/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Order>();
}
}
Concrete Implementation – Singleton HttpClient
// Configure a singleton handler with a 30‑minute lifetime
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(30),
// Optional: enable automatic keep‑alive
EnableMultipleHttp2Connections = true
};
// Register the singleton HttpClient
services.AddSingleton<HttpClient>(sp =>
{
var client = new HttpClient(handler, disposeHandler: false) // handler lives longer
{
BaseAddress = new Uri("https://api.example.com/"),
Timeout = TimeSpan.FromSeconds(30)
};
return client;
});
Use the client directly in services:
public class InventoryService
{
private readonly HttpClient _http;
public InventoryService(HttpClient http) => _http = http;
public async Task<Inventory> GetInventoryAsync(string sku)
{
var response = await _http.GetAsync($"inventory/{sku}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Inventory>();
}
}
Concrete Implementation – Per‑Call Creation (Not Recommended)
public async Task<T> CallApiAsync<T>(string url)
{
using var client = new HttpClient();
client.BaseAddress = new Uri(url);
var response = await client.GetAsync("data");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>();
}
Validation Checklist
- Handler Rotation: Create an integration test that resolves a hostname whose IP changes during the test. Verify that after
PooledConnectionLifetimethe new IP is used. - Connection Reuse: Run a load test and monitor OS socket tables. A properly pooled client should show low
TIME_WAITcounts. - Thread Safety: Ensure no code mutates
DefaultRequestHeaderson a shared client. Useclient.DefaultRequestHeaders.Addonly during configuration. - Testing: For typed clients, inject a mock
HttpMessageHandlerviaAddHttpMessageHandlerand assert that the handler receives the expected requests.
Summary
Use IHttpClientFactory typed clients when you need per‑client policies, DNS refresh, and testability. Opt for a singleton HttpClient only if your application is small, has few endpoints, and you can manage resilience manually. Avoid per‑call creation in production because it leads to socket exhaustion and poor performance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.