Stop newing up HttpClient
Why new HttpClient() in a using block causes trouble under load, and the two correct ways to call HTTP APIs in modern .NET.
Almost every integration I've inherited has had this somewhere in it:
public async Task<Order?> GetOrderAsync(int id)
{
using var client = new HttpClient();
return await client.GetFromJsonAsync<Order>($"https://api.example.com/orders/{id}");
}
It looks responsible. HttpClient is IDisposable, so we dispose it. It works on your machine, passes code review, and then falls over in production when traffic picks up.
What goes wrong
Disposing an HttpClient also disposes its handler, which closes the underlying TCP connection. Closed connections sit in the TIME_WAIT state for a while before the operating system releases the port. Create a client per request under load and you can run out of sockets. The error is usually a SocketException that has nothing obvious to do with the code that caused it.
The common "fix" is a single static readonly HttpClient. That solves socket exhaustion but creates a quieter problem: a connection that stays open forever never sees DNS changes, so after a failover or a blue-green deployment you keep talking to the old server.
Option 1: IHttpClientFactory (use this in ASP.NET Core)
The factory pools the handlers behind your clients and recycles them on a timer (two minutes by default). You get connection reuse and fresh DNS without managing either yourself. A typed client keeps the configuration in one place:
// Program.cs
builder.Services.AddHttpClient<OrdersClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});
public sealed class OrdersClient(HttpClient http)
{
public Task<Order?> GetOrderAsync(int id) =>
http.GetFromJsonAsync<Order>($"orders/{id}");
}
Inject OrdersClient wherever you need it. Don't dispose the HttpClient yourself; the factory owns its lifetime.
Watch out: typed clients are registered as transient. If you inject one into a singleton, the singleton holds on to one handler forever and you're back to the stale DNS problem.
Option 2: one long-lived client with a connection lifetime
Outside of dependency injection, in a console app or an Azure Function without the DI setup, share a single client and tell its handler to recycle connections:
private static readonly HttpClient Http = new(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
Which one to pick
| Situation | Use |
|---|---|
| ASP.NET Core, worker services, anything with DI | IHttpClientFactory with typed clients |
| Console apps and small tools | A static HttpClient with PooledConnectionLifetime set |
| Retries, timeouts and circuit breakers needed | The factory, plus AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience |
Takeaway
HttpClient is meant to be reused. Let IHttpClientFactory manage it when you have dependency injection, and set PooledConnectionLifetime when you don't.