How to make concurrent requests with HttpClient in C#

By FoxLearn 1/21/2025 8:01:10 AM   27
The HttpClient class is designed for making concurrent requests. It is thread-safe, meaning you can send multiple requests at the same time, whether from a single thread or multiple threads.

Use a Single Instance of HttpClient

To take full advantage of HttpClient's ability to handle concurrent requests efficiently, you should use a single instance of HttpClient for the entire application lifecycle. This avoids the overhead of creating new sockets for each request, which can be costly.

// Incorrect usage: creating a new HttpClient instance for each request
using (var client = new HttpClient())
{
    var response = await client.GetAsync(url);
    // Handle the response
}

This leads to multiple socket connections being created, which can quickly exhaust available ports.

Instead, use a single instance throughout your program:

// Correct usage: reuse a single HttpClient instance
private static readonly HttpClient client = new HttpClient();

Set a Limit on Concurrent Requests per Server

In some cases, it may be necessary to limit the number of concurrent requests to a particular server. This can be controlled through the MaxConnectionsPerServer property in a SocketsHttpHandler.

var handler = new SocketsHttpHandler()
{
    MaxConnectionsPerServer = 5 // Limit to 5 concurrent connections per server
};
var httpClient = new HttpClient(handler);

This ensures that no more than 5 requests are sent concurrently to the same server. If there are more requests, they will wait in a queue until a socket becomes available.

In .NET Core applications, you should use SocketsHttpHandler.MaxConnectionsPerServer to set the maximum number of concurrent connections per server. In contrast, for .NET Framework applications, you can set the maximum concurrency by configuring ServicePointManager as shown below:

private void SetMaxConcurrency(string url, int maxConcurrentRequests)
{
    ServicePointManager.FindServicePoint(new Uri(url)).ConnectionLimit = maxConcurrentRequests;
}

If you don't explicitly configure this setting, it defaults to the ServicePointManager.DefaultConnectionLimit. For ASP.NET applications, the default is 10, while for other types of applications, it’s set to 2.

Avoid Port Exhaustion - Don’t Use HttpClient as a Request Queue

When making many requests concurrently, you might encounter issues if requests are timing out or failing. HttpClient may open new sockets for each failed request, leading to port exhaustion.

A better approach is to manage queuing yourself and implement a circuit breaker strategy to prevent further requests after a failure threshold is reached.

Only Use DefaultRequestHeaders for Headers That Don’t Change

The DefaultRequestHeaders property in HttpClient is not thread-safe. You should only use it for headers that remain constant across all requests, such as User-Agent.

For dynamic headers, like an authentication token, set them individually for each request:

using (var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com"))
{
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "your_token_here");
    var response = await client.SendAsync(request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

Making Concurrent Requests to Fetch Weather Data

Let's say you want to fetch weather data for multiple cities concurrently.

You'll send requests to a weather API for cities like New York, Los Angeles, and Chicago, limiting the concurrency to 3 requests at a time.

public class WeatherService
{
    private readonly HttpClient _httpClient;
    private readonly SemaphoreSlim _semaphore;
    private long _circuitStatus;
    private const long Closed = 0;
    private const long Tripped = 1;
    public string Unavailable = "Unavailable";
    
    public WeatherService(string apiUrl, int maxConcurrentRequests)
    {
        var handler = new SocketsHttpHandler() { MaxConnectionsPerServer = maxConcurrentRequests };
        _httpClient = new HttpClient(handler);
        _semaphore = new SemaphoreSlim(maxConcurrentRequests);
        _circuitStatus = Closed;
    }

    private bool IsTripped() => Interlocked.Read(ref _circuitStatus) == Tripped;

    private void TripCircuit(string reason)
    {
        if (Interlocked.CompareExchange(ref _circuitStatus, Tripped, Closed) == Closed)
        {
            Console.WriteLine($"Circuit tripped due to: {reason}");
        }
    }

    public async Task<string> GetWeatherData(string url)
    {
        try
        {
            await _semaphore.WaitAsync();

            if (IsTripped())
                return Unavailable;

            var response = await _httpClient.GetAsync(url);

            if (!response.IsSuccessStatusCode)
            {
                TripCircuit($"Failed with status code: {response.StatusCode}");
                return Unavailable;
            }

            return await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex) when (ex is OperationCanceledException || ex is TaskCanceledException)
        {
            Console.WriteLine("Request timed out");
            TripCircuit("Timed out");
            return Unavailable;
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

Sending Concurrent Requests:

var service = new WeatherService("http://api.weather.com/v1/weather", maxConcurrentRequests: 3);

var cities = new[] { "New York", "Los Angeles", "Chicago", "Miami", "Dallas", "Seattle" };

foreach (var city in cities)
{
    Task.Run(async () =>
    {
        var weatherUrl = $"http://api.weather.com/v1/weather?city={city}";
        Console.WriteLine($"Requesting weather data for {city}");
        Console.WriteLine(await service.GetWeatherData(weatherUrl));
    });
}

In this example:

  • Concurrency Control: 3 requests are processed at the same time. Additional requests will wait in the semaphore queue.
  • Failure Handling: If any request fails (e.g., due to timeout), the circuit will be tripped, and further requests will return "Unavailable."
  • Weather Data Fetching: Weather data is fetched for each city concurrently, ensuring efficient use of resources.