IHttpClientFactory: Stop Creating HttpClient Per Request
new HttpClient() per request looks stateless and is the opposite. Each instance owns a handler that owns sockets. Dispose the client and the socket sits in TIME_WAIT. Under a burst — a fan-out to a partner API, a loop over tenants — the process runs out of ephemeral ports. The exception is a connection failure, which sends people looking at the remote service. The remote service is fine. The caller used up its own ports.
A single static HttpClient for the life of the process avoids that, and then hits the other bug. The handler caches DNS. When the dependency moves behind a new address, your process keeps calling the old one until it restarts. That shows up after a failover, not in the first week.
The factory is the middle path
IHttpClientFactory pools the handlers and recycles them on a timer, so sockets are reused and DNS is picked up again. Register it once and inject IHttpClientFactory, or inject a typed client.
builder.Services.AddHttpClient("payments", client =>
{
client.BaseAddress = new Uri(builder.Configuration["Payments:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(10);
});
public sealed class PaymentClient(IHttpClientFactory factory)
{
public Task<HttpResponseMessage> GetAsync(string id, CancellationToken cancellationToken)
{
var client = factory.CreateClient("payments");
return client.GetAsync($"charges/{id}", cancellationToken);
}
}
Do not dispose the client you get from the factory. Disposal returns the handler to the pool in a way the factory did not ask for. Create, send, read the content, move on.
Timeout, retry, and the handler lifetime
The default handler lifetime is two minutes. That is the DNS refresh interval. Shorten it only if a failover must be visible faster and you have measured the extra connection setup. Lengthen it only if you have a reason the docs do not.
Set Timeout on the client to something your callers can survive. The default is 100 seconds, which will pin a request thread, or a queued work item, long after the user is gone. Ten seconds plus a bounded retry at the call site is easier to operate than one silent 100-second hang.
Retries belong on a policy (Polly, or the resilience handler in the same AddHttpClient registration), and they must not retry a non-idempotent POST unless you send an idempotency key. A retried payment without a key is a second charge. The socket fix does not make the HTTP method safe.
What to look at when it still fails
SocketExceptionor "only one usage of each socket address" in the logs: something is still constructingHttpClientby hand. Search the repo fornew HttpClient.- Calls stuck on the old IP after a deploy of the dependency: the handler lifetime has not elapsed, or a static client is still in the process. Restart is a workaround. The factory is the fix.
- Timeouts that are really the server taking 30 seconds: the client timeout is doing its job. Fix the dependency or stop calling it on the user path.
On Azure App Service or Container Apps the symptom is the same, because the limit is in the process, not in the platform. One named client per dependency, created through the factory, is the whole pattern.
Keep reading
Fixing NuGet Error: Unable to Load the Service Index for Source
A step-by-step guide to diagnosing and resolving NuGet package source errors in Visual Studio and dotnet CLI.
Pass the CancellationToken or the Work Continues After the Client Left
ASP.NET Core binds CancellationToken to the aborted request. Forward it to EF Core and HttpClient, and do not turn a client disconnect into a 500.
Azure Functions vs Azure Container Apps: Choosing the Right Serverless Model
A detailed comparison of Azure Functions and Azure Container Apps — pricing, cold starts, scaling, runtime support, and a decision flowchart for picking the right one.
Rate Limiting and Throttling: Designing APIs That Survive Traffic Spikes
Why every public API needs rate limiting, the four main algorithms compared, implementation in Next.js with Redis, proper HTTP headers, and Azure API Management policies.
CQRS in Practice: When It's Worth the Complexity and When It Isn't
A clear-eyed look at Command Query Responsibility Segregation — what it actually is, how to implement it in .NET, when it pays off, and when it's unnecessary complexity.
Monolith to Microservices: How to Decompose Without Creating a Distributed Monolith
A practical guide to breaking apart a monolith into microservices — identifying boundaries, splitting the database, handling data consistency, and avoiding the traps that turn your migration into a distributed monolith.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.