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

HttpClient.Timeout, or the resilience pipeline’s total-request timeout strategy (preferred, since it composes with retries correctly).

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 GET) should retry automatically — a non-idempotent POST retried after a timeout can duplicate the effect.

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 a using block disposing one per call) can exhaust available sockets under load, because each disposal does not immediately release the underlying TCP connection (TIME_WAIT). IHttpClientFactory avoids this by pooling and periodically recycling handlers behind the scenes, while HttpClient instances it hands out remain cheap to keep or discard.

  • A single long-lived HttpClient with 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.

  • DefaultRequestHeaders on a pooled client — setting client.DefaultRequestHeaders on an IHttpClientFactory-created client is not safe to do per-call, since the same HttpClient instance/config can be reused or reset between calls depending on client type; set static headers once in the client’s configuration delegate (as with X-Api-Key above) or per-request via HttpRequestMessage.Headers instead.

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.