Using HttpClient
efficiently is crucial for building robust C# applications. Poor usage patterns can lead to socket exhaustion, memory leaks, and performance issues. Here are the essential best practices for working with HttpClient
instances:
1. Prefer IHttpClientFactory Over Manual Instance Management
Modern Approach (Recommended): Use IHttpClientFactory
in .NET Core/.NET 5+ applications:
// Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Or configure named clients
services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
});
}
// Service class
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("api");
}
public async Task<string> GetDataAsync(string endpoint)
{
using var response = await _httpClient.GetAsync(endpoint);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
Legacy Applications: If you can't use IHttpClientFactory
, use a static/singleton instance:
public static class HttpClientInstance
{
private static readonly Lazy<HttpClient> _httpClient = new(() =>
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
client.Timeout = TimeSpan.FromSeconds(30);
return client;
});
public static HttpClient Instance => _httpClient.Value;
}
2. Always Use using
Statements with Responses
Always dispose of HttpResponseMessage
to free resources:
// ✅ Correct - automatically disposes response
public async Task<string> GetContentAsync(string url)
{
using var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
// ❌ Incorrect - memory leak potential
public async Task<string> GetContentBadAsync(string url)
{
var response = await _httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync(); // Response not disposed
}
3. Implement Comprehensive Error Handling
Handle different types of exceptions that can occur during HTTP operations:
public async Task<ApiResult<T>> GetAsync<T>(string endpoint)
{
try
{
using var response = await _httpClient.GetAsync(endpoint);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<T>(json);
return ApiResult<T>.Success(data);
}
return ApiResult<T>.Failure($"HTTP {(int)response.StatusCode}: {response.ReasonPhrase}");
}
catch (HttpRequestException ex)
{
// Network-related errors
return ApiResult<T>.Failure($"Network error: {ex.Message}");
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// Request timeout
return ApiResult<T>.Failure("Request timed out");
}
catch (TaskCanceledException)
{
// Request was cancelled
return ApiResult<T>.Failure("Request was cancelled");
}
catch (JsonException ex)
{
// JSON parsing errors
return ApiResult<T>.Failure($"Invalid response format: {ex.Message}");
}
}
4. Configure Timeouts and Cancellation
Always use cancellation tokens and appropriate timeouts:
public async Task<string> GetWithTimeoutAsync(string url, int timeoutSeconds = 30)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
try
{
using var response = await _httpClient.GetAsync(url, cts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
{
throw new TimeoutException($"Request to {url} timed out after {timeoutSeconds} seconds");
}
}
// For methods that accept CancellationToken
public async Task<string> GetAsync(string url, CancellationToken cancellationToken = default)
{
using var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
5. Optimize for Different Scenarios
For High-Volume Applications
Configure connection limits and reuse:
services.AddHttpClient("high-volume", client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
MaxConnectionsPerServer = 10,
UseCookies = false // Disable if not needed for better performance
});
For Web Scraping
Configure user agents and headers to avoid blocking:
services.AddHttpClient("scraper", client =>
{
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
client.DefaultRequestHeaders.Add("Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
client.Timeout = TimeSpan.FromSeconds(45);
});
6. Testing Best Practices
Use HttpMessageHandler
mocking for unit tests:
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>> _sendAsync;
public MockHttpMessageHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> sendAsync)
{
_sendAsync = sendAsync;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return _sendAsync(request);
}
}
// In your test
[Test]
public async Task GetDataAsync_ReturnsExpectedResult()
{
var mockHandler = new MockHttpMessageHandler(request =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"id\":1,\"name\":\"test\"}", Encoding.UTF8, "application/json")
};
return Task.FromResult(response);
});
var httpClient = new HttpClient(mockHandler);
var service = new ApiService(httpClient);
var result = await service.GetDataAsync("test-endpoint");
Assert.IsNotNull(result);
}
7. Performance Monitoring
Add logging and metrics for production monitoring:
public class LoggingDelegatingHandler : DelegatingHandler
{
private readonly ILogger<LoggingDelegatingHandler> _logger;
public LoggingDelegatingHandler(ILogger<LoggingDelegatingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
_logger.LogInformation("Starting HTTP request to {Url}", request.RequestUri);
try
{
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
_logger.LogInformation(
"HTTP request completed in {ElapsedMs}ms with status {StatusCode}",
stopwatch.ElapsedMilliseconds,
response.StatusCode);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex,
"HTTP request failed after {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
// Register the handler
services.AddHttpClient("logged")
.AddHttpMessageHandler<LoggingDelegatingHandler>();
Common Anti-Patterns to Avoid
❌ Don't create new HttpClient instances per request
// This causes socket exhaustion
public async Task<string> BadMethod(string url)
{
using var client = new HttpClient(); // ❌ Creates new instance each time
return await client.GetStringAsync(url);
}
❌ Don't ignore response disposal
// Memory leak potential
var response = await _httpClient.GetAsync(url); // ❌ Not disposed
var content = await response.Content.ReadAsStringAsync();
return content;
❌ Don't use synchronous methods in async contexts
// Blocks threads and can cause deadlocks
var result = _httpClient.GetStringAsync(url).Result; // ❌ Blocking async call
By following these best practices, you'll build more reliable, performant, and maintainable applications that properly utilize HttpClient
resources.