HTTP client in C#: making requests correctly — advanced level
In modern .NET development, working with HTTP requests goes far beyond simply calling WebClient or basic HttpClient. At an advanced level, you need to consider connection pool management, proper error handling, timeouts, retry policies, and asynchronous execution. In this article, we will cover best practices for working with an HTTP client in C# that will help you build reliable and high-performance applications.
1. Proper work with HttpClient and connection pool
One of the most common mistakes is creating a new instance of HttpClient for each request. This leads to socket exhaustion because each instance keeps an open TCP connection in the TIME_WAIT state.
Incorrect:
public async Task<string> GetDataAsync(string url){ using (var client = new HttpClient()) { return await client.GetStringAsync(url); }}Correct: use a single static or singleton instance of HttpClient.
public class HttpService{ private static readonly HttpClient _client = new HttpClient();
public async Task<string> GetDataAsync(string url) { return await _client.GetStringAsync(url); }}For finer control, use IHttpClientFactory from the Microsoft.Extensions.Http package. The factory manages the pool and automatically recovers connections during DNS failures.
// In Startup.cs or Program.csservices.AddHttpClient("MyClient", client =>{ client.BaseAddress = new Uri("https://api.example.com"); client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");});
// In a servicepublic class MyService{ private readonly IHttpClientFactory _httpClientFactory;
public MyService(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; }
public async Task<string> GetDataAsync() { var client = _httpClientFactory.CreateClient("MyClient"); return await client.GetStringAsync("/data"); }}2. Timeouts and request cancellation
Without timeouts, your client can hang indefinitely. HttpClient has two mechanisms:
- Timeout — a general timeout for the entire request.
- CancellationToken — for canceling a request externally (e.g., the user closed the application).
Set Timeout reasonably. For most APIs, 10–30 seconds is suitable.
var client = new HttpClient{ Timeout = TimeSpan.FromSeconds(15)};
// Example with CancellationTokenSourceusing var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));try{ var response = await client.GetAsync("https://slow-api.example.com", cts.Token);}catch (TaskCanceledException){ Console.WriteLine("Request cancelled due to timeout.");}Important: Timeout and CancellationToken work together. If the CancellationToken fires earlier, the request is cancelled faster.
3. Error handling and retry policy
Network failures and temporary server errors are common. Instead of immediately failing, implement a retry policy with exponential backoff. The Polly library is excellent for this.
using Polly;using Polly.Extensions.Http;
// Define the retry policyvar retryPolicy = HttpPolicyExtensions .HandleTransientHttpError() // 5xx, 408 .Or<HttpRequestException>() .WaitAndRetryAsync(3, retryAttempt => Tim