HTTP Client and Resilience
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
IHttpClientFactory is the supported way to create HttpClient instances in ASP.NET Core — it manages the
underlying connection pool, wires up DelegatingHandler chains, and avoids the pitfalls of hand-managed
HttpClient lifetimes.
IHttpClientFactory: named and typed clients
// named client -- resolved by string key
builder.Services.AddHttpClient("orders", c =>
{
c.BaseAddress = new Uri("https://orders.internal");
c.Timeout = TimeSpan.FromSeconds(10);
});
public sealed class OrdersReader(IHttpClientFactory factory)
{
public Task<Order?> GetAsync(int id)
=> factory.CreateClient("orders").GetFromJsonAsync<Order>($"/orders/{id}");
}
// typed client -- HttpClient injected into a purpose-built wrapper, resolved by its own type
builder.Services.AddHttpClient<OrdersApiClient>(c => c.BaseAddress = new Uri("https://orders.internal"));
public sealed class OrdersApiClient(HttpClient http)
{
public Task<Order?> GetAsync(int id) => http.GetFromJsonAsync<Order>($"/orders/{id}");
}
public sealed class OrdersController(OrdersApiClient orders) : ControllerBase { } // inject the typed client directly
Typed clients are generally preferred: the client’s API is discoverable and testable behind an interface,
rather than a stringly-typed factory lookup scattered across the app. See
Make HTTP requests using
IHttpClientFactory.
HttpMessageHandler chains
Each named/typed client can add DelegatingHandler`s that wrap the outgoing request/response — the `HttpClient
analog of middleware:
public sealed class ApiKeyHandler(IOptions<ApiOptions> options) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
request.Headers.Add("X-Api-Key", options.Value.ApiKey);
return await base.SendAsync(request, ct);
}
}
builder.Services.AddTransient<ApiKeyHandler>();
builder.Services.AddHttpClient<OrdersApiClient>(c => c.BaseAddress = new Uri("https://orders.internal"))
.AddHttpMessageHandler<ApiKeyHandler>();
Handlers run in registration order on the way out and reverse order on the way back, same as middleware.
IHttpClientFactory pools and recycles the underlying HttpMessageHandler chain automatically, which is the
main problem it solves — see the socket-exhaustion pitfall below.
Resilience: Microsoft.Extensions.Http.Resilience (Polly)
// dotnet add package Microsoft.Extensions.Http.Resilience
builder.Services.AddHttpClient<OrdersApiClient>(c => c.BaseAddress = new Uri("https://orders.internal"))
.AddStandardResilienceHandler(o =>
{
o.Retry.MaxRetryAttempts = 3;
o.CircuitBreaker.FailureRatio = 0.5;
o.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
});
AddStandardResilienceHandler layers, in order, a per-attempt timeout, retry with exponential backoff and
jitter, a circuit breaker, and an overall-request timeout — a Polly-based pipeline preconfigured with sensible
defaults for outbound HTTP calls. Use AddResilienceHandler instead to compose a custom pipeline (a different
strategy mix, or non-default trigger conditions) from the same building blocks. See
Build resilient HTTP apps: Key
development patterns and the Polly documentation it builds on.
Timeouts, retries, and circuit breakers
| Concern | Where it’s set |
|---|---|
Overall request timeout |
|
Per-attempt timeout |
The resilience pipeline’s attempt timeout — bounds a single try, letting a retry get a fresh budget rather than inheriting whatever time the first attempt used. |
Retry |
Exponential backoff with jitter by default; only idempotent operations (typically |
Circuit breaker |
Trips after a failure-ratio threshold, so a struggling downstream service stops receiving a flood of doomed requests and gets time to recover. |
HttpClient pitfalls
-
Socket exhaustion —
new HttpClient()per request (or ausingblock disposing one per call) can exhaust available sockets under load, because each disposal does not immediately release the underlying TCP connection (TIME_WAIT).IHttpClientFactoryavoids this by pooling and periodically recycling handlers behind the scenes, whileHttpClientinstances it hands out remain cheap to keep or discard. -
A single long-lived
HttpClientwith no factory — avoids socket exhaustion but then never observes DNS changes for its target, since a handler’s connection pool is bound to the resolved addresses from when it was created. `IHttpClientFactory’s periodic handler rotation exists specifically to balance both concerns. -
DefaultRequestHeaderson a pooled client — settingclient.DefaultRequestHeaderson anIHttpClientFactory-created client is not safe to do per-call, since the sameHttpClientinstance/config can be reused or reset between calls depending on client type; set static headers once in the client’s configuration delegate (as withX-Api-Keyabove) or per-request viaHttpRequestMessage.Headersinstead.
Generated clients: Refit and Kiota
// dotnet add package Refit.HttpClientFactory
public interface IOrdersApi
{
[Get("/orders/{id}")]
Task<Order> GetAsync(int id);
}
builder.Services.AddRefitClient<IOrdersApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://orders.internal"));
Refit turns a hand-declared interface into an HttpClient-backed implementation via source generation, and
registers cleanly with IHttpClientFactory. Kiota-generated clients (see
OpenAPI and API Versioning) are generated from an OpenAPI
document instead of hand-written, and also compose with the resilience handlers above through their own
IHttpClientFactory integration.