Table of contents

How do I improve the performance of HttpClient (C#) in a high-load application?

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.

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