Optimizing HttpClient
performance in high-load C# applications requires careful attention to connection management, resource usage, and concurrency patterns. Poor implementation can lead to socket exhaustion, memory leaks, and degraded performance. Here's a comprehensive guide to maximize performance.
1. Proper HttpClient Instantiation
❌ Anti-Pattern: Creating New Instances
// DON'T DO THIS - causes socket exhaustion
public async Task<string> BadExample(string url)
{
using var client = new HttpClient(); // Creates new connection each time
return await client.GetStringAsync(url);
}
✅ Static HttpClient (Simple Scenarios)
For simple applications, reuse a static instance:
public class HttpService
{
private static readonly HttpClient _httpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(30)
};
public async Task<string> GetAsync(string url)
{
using var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
✅ HttpClientFactory (Recommended)
Use IHttpClientFactory
for better DNS handling and configuration:
// Startup.cs / Program.cs
services.AddHttpClient<ApiService>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
// Service class
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<T> GetAsync<T>(string endpoint)
{
using var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(json);
}
}
2. Advanced Configuration
Connection Pooling and Limits
services.AddHttpClient<ApiService>()
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
MaxConnectionsPerServer = 100, // Increase for high throughput
UseDefaultCredentials = false,
UseCookies = false // Disable if not needed for better performance
});
HTTP/2 and HTTP/3 Support
services.AddHttpClient<ApiService>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler()
{
PooledConnectionLifetime = TimeSpan.FromMinutes(15),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 100
})
.ConfigureHttpClient(client =>
{
client.DefaultRequestVersion = HttpVersion.Version20;
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
});
3. Efficient Request Handling
Streaming Large Responses
public async Task ProcessLargeResponseAsync(string url)
{
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
// Process line by line instead of loading entire response
ProcessLine(line);
}
}
Efficient JSON Handling
public async Task<T> GetJsonAsync<T>(string url)
{
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<T>(stream);
}
4. Concurrency Management
Throttling with SemaphoreSlim
public class ThrottledHttpService
{
private readonly HttpClient _httpClient;
private readonly SemaphoreSlim _semaphore;
public ThrottledHttpService(HttpClient httpClient, int maxConcurrency = 10)
{
_httpClient = httpClient;
_semaphore = new SemaphoreSlim(maxConcurrency);
}
public async Task<string> GetWithThrottlingAsync(string url)
{
await _semaphore.WaitAsync();
try
{
using var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
finally
{
_semaphore.Release();
}
}
}
Batch Processing with Concurrency Control
public async Task<IEnumerable<T>> ProcessUrlsBatchAsync<T>(
IEnumerable<string> urls,
Func<string, Task<T>> processor,
int maxConcurrency = 10)
{
var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync();
try
{
return await processor(url);
}
finally
{
semaphore.Release();
}
});
return await Task.WhenAll(tasks);
}
5. Connection Pool Optimization
ServicePointManager (Legacy .NET Framework)
// Configure before any HttpClient usage
ServicePointManager.DefaultConnectionLimit = 100;
ServicePointManager.Expect100Continue = false;
ServicePointManager.UseNagleAlgorithm = false;
ServicePointManager.EnableDnsRoundRobin = true;
ServicePointManager.DnsRefreshTimeout = 60000; // 1 minute
Modern .NET Connection Settings
services.AddHttpClient<ApiService>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler()
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 50,
EnableMultipleHttp2Connections = true,
RequestHeaderEncodingSelector = (name, request) =>
name == "User-Agent" ? Encoding.UTF8 : null
});
6. Error Handling and Resilience
Retry Policies with Polly
services.AddHttpClient<ApiService>()
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetTimeoutPolicy());
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.Or<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, duration, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} after {duration}s");
});
}
private static IAsyncPolicy<HttpResponseMessage> GetTimeoutPolicy()
{
return Policy.TimeoutAsync<HttpResponseMessage>(10);
}
7. Performance Monitoring
Custom Logging Handler
public class PerformanceLoggingHandler : DelegatingHandler
{
private readonly ILogger<PerformanceLoggingHandler> _logger;
public PerformanceLoggingHandler(ILogger<PerformanceLoggingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
_logger.LogInformation(
"HTTP {Method} {Uri} responded {StatusCode} in {ElapsedMs}ms",
request.Method,
request.RequestUri,
(int)response.StatusCode,
stopwatch.ElapsedMilliseconds);
return response;
}
}
// Register the handler
services.AddTransient<PerformanceLoggingHandler>();
services.AddHttpClient<ApiService>()
.AddHttpMessageHandler<PerformanceLoggingHandler>();
8. Memory Management Best Practices
Proper Disposal Patterns
public async Task<ApiResponse> GetApiDataAsync(string endpoint)
{
// Use 'using' for automatic disposal
using var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
// Read content efficiently
var content = await response.Content.ReadAsStringAsync();
return new ApiResponse
{
Data = content,
StatusCode = response.StatusCode,
Headers = response.Headers.ToDictionary(h => h.Key, h => h.Value.First())
};
}
Performance Checklist
✅ Use HttpClientFactory instead of manual instantiation
✅ Configure appropriate connection limits (50-100 per server)
✅ Implement concurrency throttling with SemaphoreSlim
✅ Use HTTP/2 when supported by target servers
✅ Stream large responses with ResponseHeadersRead
✅ Dispose HttpResponseMessage properly
✅ Set reasonable timeouts (10-30 seconds)
✅ Implement retry policies for resilience
✅ Monitor connection pool metrics
✅ Disable cookies if not needed
Following these practices will ensure your HttpClient implementation can handle high-load scenarios efficiently while maintaining optimal resource usage and response times.