Table of contents

What are the best practices for using HttpClient (C#) instances?

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.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon