Reusing HttpClient
instances effectively is crucial for performance and preventing socket exhaustion in C# applications. Creating a new HttpClient
for each request can lead to SocketException
errors under heavy load due to port exhaustion.
Why HttpClient Reuse Matters
Each HttpClient
instance manages its own connection pool. Creating multiple instances unnecessarily:
- Consumes system resources
- Prevents connection pooling benefits
- Can exhaust available TCP ports
- Increases DNS lookup overhead
Recommended Approaches
1. IHttpClientFactory (Preferred for ASP.NET Core)
IHttpClientFactory
is the modern, recommended approach for managing HttpClient
instances in ASP.NET Core applications. It handles connection pooling, DNS changes, and lifecycle management automatically.
// Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
// Basic registration
services.AddHttpClient();
// Named client with configuration
services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Typed client registration
services.AddHttpClient<WeatherService>();
}
// Using named client
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("ApiClient");
}
public async Task<string> GetDataAsync()
{
var response = await _httpClient.GetAsync("data");
return await response.Content.ReadAsStringAsync();
}
}
// Using typed client
public class WeatherService
{
private readonly HttpClient _httpClient;
public WeatherService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.weather.com/");
}
public async Task<string> GetWeatherAsync(string city)
{
var response = await _httpClient.GetAsync($"weather/{city}");
return await response.Content.ReadAsStringAsync();
}
}
2. Static HttpClient (For Simple Scenarios)
For simple console applications or when not using dependency injection:
public static class HttpClientProvider
{
private static readonly HttpClient _httpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(30)
};
static HttpClientProvider()
{
_httpClient.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
}
public static HttpClient Instance => _httpClient;
}
// Usage
public class DataService
{
public async Task<string> FetchDataAsync(string url)
{
var client = HttpClientProvider.Instance;
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}
3. Singleton Pattern with Lazy Initialization
For thread-safe lazy initialization:
public sealed class HttpClientSingleton
{
private static readonly Lazy<HttpClient> _lazyHttpClient =
new Lazy<HttpClient>(() => CreateHttpClient());
public static HttpClient Instance => _lazyHttpClient.Value;
private static HttpClient CreateHttpClient()
{
var client = new HttpClient();
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
client.Timeout = TimeSpan.FromSeconds(30);
return client;
}
}
// Usage
var response = await HttpClientSingleton.Instance.GetAsync("endpoint");
Advanced Configuration Examples
Multiple Clients with Different Configurations
services.AddHttpClient("AuthorizedClient", client =>
{
client.BaseAddress = new Uri("https://secure-api.example.com/");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "your-token");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
UseCookies = false,
UseProxy = false
});
services.AddHttpClient("PublicClient", client =>
{
client.BaseAddress = new Uri("https://public-api.example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});
With Polly for Resilience
services.AddHttpClient("ResilientClient")
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30));
}
What NOT to Do
// ❌ DON'T: Create new HttpClient for each request
public async Task<string> BadExample(string url)
{
using var client = new HttpClient(); // Creates new connection pool each time
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
// ❌ DON'T: Dispose static HttpClient
public static class BadSingleton
{
public static HttpClient Client = new HttpClient();
public static void Cleanup()
{
Client.Dispose(); // Don't dispose static instances
}
}
Best Practices Summary
- Use
IHttpClientFactory
in ASP.NET Core applications - Create long-lived instances for simple scenarios
- Configure timeouts to prevent hanging requests
- Set appropriate headers once during initialization
- Don't dispose static or singleton instances
- Use typed clients for better organization and testing
- Consider resilience patterns like retry and circuit breaker
- Monitor connection pool usage in production
By following these patterns, you'll achieve optimal performance while avoiding common pitfalls like socket exhaustion and DNS resolution issues.