Skip to Content
Course content

208: HttpClient Configuration and Lifetime

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been following standard C# patterns, you've likely internalized a golden rule: if a class implements IDisposable, wrap it in a using block. It feels like the responsible thing to do. You don't want memory leaks, and you certainly don't want to leave unmanaged resources hanging around. However, when it comes to HttpClient, following this rule is one of the most common ways to accidentally crash a high-traffic production server.

The "Dispose it Every Time" Trap

I've seen this countless times in code reviews. A developer writes a service to fetch data from an external API—let's say a stock price ticker—and writes it like this:

public async Task<decimal> GetStockPrice(string symbol)
{
    using (var client = new HttpClient())
    {
        var response = await client.GetStringAsync($"https://api.stocks.com/v1/quote?s={symbol}");
        return ParsePrice(response);
    }
}

On the surface, this looks perfect. It's clean, it's scoped, and the client is disposed of immediately after the call. But here is the catch: disposing of the HttpClient does not immediately close the underlying TCP socket. The operating system keeps that socket in a TIME_WAIT state for a period of time to ensure any late-arriving packets are handled correctly.

If your application is under heavy load—say, processing 500 requests per second—you will spin up 500 clients and dispose of them, but those 500 sockets stay open in the OS. Very quickly, you'll hit the "socket exhaustion" limit. Your app will start throwing SocketException errors, not because the API is down, but because your own server has run out of available ports to talk to the outside world. I've spent a few late nights debugging "intermittent network failures" only to realize the code was too diligent about disposing of its clients.

Long-Lived Instances and DNS Stale-Out

The immediate fix is usually to make the HttpClient a static or singleton instance. By reusing one client for the life of the application, you reuse the same underlying sockets, and the socket exhaustion problem vanishes.

But wait—there's a second trap. If you use a single static HttpClient for the entire life of a long-running process, that client never checks for DNS changes. If the API you're calling updates its IP address (which happens often in cloud environments with blue-green deployments or failovers), your static client will keep trying to hit the old, dead IP address until you restart your entire application. It's a classic "out of the frying pan, into the fire" scenario.

Letting IHttpClientFactory Handle the Heavy Lifting

In modern .NET, we solve both of these problems using IHttpClientFactory. This is the professional way to handle lifetimes. Instead of you managing the HttpClient object, the factory manages a pool of HttpMessageHandler objects. It rotates them periodically to ensure DNS records are refreshed, but it keeps them alive long enough to prevent socket exhaustion.

You typically configure this in your Program.cs or Startup.cs. I highly recommend using "Typed Clients." This allows you to encapsulate all the configuration—like base addresses and default headers—into a specific class rather than scattering strings all over your business logic.

// Registration in Program.cs
builder.Services.AddHttpClient<StockService>(client =>
{
    client.BaseAddress = new Uri("https://api.stocks.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
    client.Timeout = TimeSpan.FromSeconds(5);
});

// The actual service implementation
public class StockService
{
    private readonly HttpClient _httpClient;

    public StockService(HttpClient httpClient)
    {
        // The factory provides a pre-configured client here
        _httpClient = httpClient;
    }

    public async Task<decimal> GetStockPrice(string symbol)
    {
        var response = await _httpClient.GetStringAsync($"v1/quote?s={symbol}");
        return ParsePrice(response);
    }
}

By doing this, the StockService doesn't care about the lifetime of the client; it just uses the one it was given. The framework handles the pooling and the rotation under the hood. It's cleaner, it's safer, and it won't wake you up at 3 AM with a socket exhaustion alert.




📋 Practical Task

Refactoring a Socket-Leaking Weather Service

You have inherited a legacy WeatherReportService that is causing intermittent connection errors in production. The current implementation creates a new HttpClient inside a foreach loop to fetch weather data for multiple cities.

Your Task: Refactor the provided code to use a Typed Client pattern. You must:

  1. Create a WeatherClient class that accepts an HttpClient in its constructor.
  2. Move the base URL (https://api.weather-service.net/) and the API key header (X-API-KEY: secret-key-123) into the service registration configuration.
  3. Update the WeatherReportService to use the WeatherClient instead of manually instantiating HttpClient.
// CURRENT LEAKY CODE
public class WeatherReportService 
{
    public async Task GetRegionalReport(List<string> cities) 
    {
        foreach(var city in cities) 
        {
            // This is the source of the socket exhaustion!
            using (var client = new HttpClient()) 
            {
                client.DefaultRequestHeaders.Add("X-API-KEY", "secret-key-123");
                var data = await client.GetStringAsync($"https://api.weather-service.net/weather?q={city}");
                Console.WriteLine($"Weather in {city}: {data}");
            }
        }
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.