In microservices architecture or when communicating with external APIs, the HTTP client is an essential component. However, many developers fail to adequately consider performance and usability when implementing the HTTP client.
This article will introduce best practices for using the <span>HttpClient</span>
class in C# and explore some important aspects of HTTP communication.
1. Do not create and destroy HttpClient on every request
The most common mistake beginners make is creating and destroying <span>HttpClient</span>
instances with every HTTP request.
public async Task<string> GetStringFromApi()
{
using (var client = new HttpClient())
{
return await client.GetStringAsync("https://api.example.com/data");
}
}
Why is this wrong?
Every time a new instance of <span>HttpClient</span>
is created, a new socket connection is allocated. When the client exits the <span>using</span>
statement block, the socket connection is not immediately released but enters the <span>TIME_WAIT</span>
state, which can last for several seconds.
Under high load, this can lead to socket exhaustion (<span>SocketException: Address already in use</span>
), as the operating system takes several minutes to reclaim the sockets.
2. Do not keep HttpClient as a singleton
Another common practice is to create the <span>HttpClient</span>
object as a singleton.
public class ApiClient
{
private static readonly HttpClient _client = new HttpClient();
public async Task<string> GetStringFromApi()
{
return await _client.GetStringAsync("https://api.example.com/data");
}
}
Why is this not the best choice?
<span>HttpClient</span>
is designed for long-term use, but using it as a singleton has its issues:
- •
<span>HttpClient</span>
does not automatically respond to DNS changes - • In some cases, previously set default request headers may carry over to subsequent requests
- • There is no automatic circuit breaker mechanism
- • Cannot control the maximum number of connections to the same host (which may lead to congestion)
3. Use HttpClientFactory (.NET Core 2.1 and above)
.NET Core 2.1 introduced <span>HttpClientFactory</span>
, which addresses the above issues.
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
}
// GithubService.cs
public class GithubService
{
private readonly IHttpClientFactory _clientFactory;
public GithubService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetAspNetDocsIssues()
{
var client = _clientFactory.CreateClient("github");
var response = await client.GetAsync("/repos/aspnet/AspNetCore.Docs/issues");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
<span>HttpClientFactory</span>
provides the following advantages:
- • Manages the lifecycle of the underlying
<span>HttpClientMessageHandler</span>
- • Supports reliable DNS refresh
- • Applies polling strategies to prevent connection congestion (using connections in turn rather than all at once)
- • Built-in support for Polly integration, making it easy to add circuit breakers, timeouts, retries, and other resilience policies
4. Use strongly typed clients
You can further improve the <span>HttpClientFactory</span>
method by registering strongly typed clients using the <span>AddHttpClient<TClient>()</span>
method.
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<IGithubClient, GithubClient>(c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
}
// GithubClient.cs
public interface IGithubClient
{
Task<IEnumerable<GithubIssue>> GetAspNetDocsIssues();
}
public class GithubClient : IGithubClient
{
private readonly HttpClient _httpClient;
public GithubClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<IEnumerable<GithubIssue>> GetAspNetDocsIssues()
{
var response = await _httpClient.GetAsync("/repos/aspnet/AspNetCore.Docs/issues");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<IEnumerable<GithubIssue>>(content);
}
}
The main advantages of this approach are:
- • Strongly typed interfaces
- • Dependency injection plug-and-play
- • Separation of concerns
5. Set timeouts
<span>HttpClient</span>
has a default timeout of 100 seconds, which may be too long. It is recommended to set a more reasonable timeout value.
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Set timeout to 10 seconds
c.Timeout = TimeSpan.FromSeconds(10);
});
For <span>HttpClient</span>
, setting a Timeout means that all requests will use this value by default. Different timeouts can also be set on a per-request basis.
6. Implement resilience patterns
HTTP communication is affected by various factors and may experience intermittent failures. Use resilience patterns to handle these issues:
Retry policy
services.AddHttpClient("github")
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
Circuit breaker
services.AddHttpClient("github")
.AddTransientHttpErrorPolicy(p =>
p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
Combined policy
services.AddHttpClient("github")
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)))
.AddTransientHttpErrorPolicy(p =>
p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
This combination applies both the retry policy and the circuit breaker pattern: requests may be retried 3 times after failure, but if failures persist, the circuit breaker will trigger and block further requests for 30 seconds.
7. Properly handle disposal
Although <span>HttpClient</span>
implements <span>IDisposable</span>
, there is no need to explicitly dispose of clients created by the factory when using <span>HttpClientFactory</span>
. The factory manages the client lifecycle.
// No need for using statement
public async Task<string> GetDataFromApi()
{
var client = _clientFactory.CreateClient("named-client");
return await client.GetStringAsync("/api/data");
}
8. Handle cancellation requests
Use <span>CancellationToken</span>
to allow cancellation of long-running requests:
public async Task<string> GetLongRunningDataAsync(CancellationToken cancellationToken = default)
{
var client = _clientFactory.CreateClient("named-client");
var response = await client.GetAsync("/api/longrunning", cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
Allow cancellation of requests from the controller:
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
try
{
var data = await _apiClient.GetLongRunningDataAsync(cancellationToken);
return Ok(data);
}
catch (OperationCanceledException)
{
// Request was canceled, no further processing needed
return StatusCode(499); // Client closed the request
}
}
9. Add request and response logging
Add logging for HTTP calls to assist with debugging and monitoring:
services.AddHttpClient("github")
.AddHttpMessageHandler(() => new LoggingHandler(_loggerFactory));
public class LoggingHandler : DelegatingHandler
{
private readonly ILogger _logger;
public LoggingHandler(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<LoggingHandler>();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.LogInformation("Making request to {Url}", request.RequestUri);
try
{
// Measure request time
var stopwatch = Stopwatch.StartNew();
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
_logger.LogInformation("Received response from {Url} with status code {StatusCode} in {ElapsedMilliseconds}ms",
request.RequestUri, response.StatusCode, stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error making HTTP request to {Url}", request.RequestUri);
throw;
}
}
}
10. Compression
To improve performance, especially when handling large responses, enable HTTP compression:
services.AddHttpClient("github")
.ConfigureHttpClient(client =>
{
client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
});
To handle compressed responses:
public async Task<string> GetCompressedDataAsync()
{
var client = _clientFactory.CreateClient("github");
var response = await client.GetAsync("/api/largedata");
response.EnsureSuccessStatusCode();
// HttpClient automatically handles decompression
return await response.Content.ReadAsStringAsync();
}
11. Handle authentication
Common authentication methods include:
Basic authentication
services.AddHttpClient("authenticated-client")
.ConfigureHttpClient(client =>
{
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes("username:password"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
});
Bearer token
services.AddHttpClient("authenticated-client")
.ConfigureHttpClient(client =>
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "your-access-token");
});
Dynamic authentication handling
Use the <span>HttpClientFactory</span>
’s <span>AddHttpMessageHandler</span>
method to add authentication handling:
services.AddTransient<AuthenticationHandler>();
services.AddHttpClient("authenticated-client")
.AddHttpMessageHandler<AuthenticationHandler>();
public class AuthenticationHandler : DelegatingHandler
{
private readonly ITokenService _tokenService;
public AuthenticationHandler(ITokenService tokenService)
{
_tokenService = tokenService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Dynamically get the token
var token = await _tokenService.GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
12. Handle concurrent requests
To send multiple requests in parallel:
public async Task<IEnumerable<Product>> GetProductsAsync(IEnumerable<int> productIds)
{
var client = _clientFactory.CreateClient("product-api");
var tasks = productIds.Select(id =>
client.GetFromJsonAsync<Product>($"/api/products/{id}"));
return await Task.WhenAll(tasks);
}
However, be careful to avoid starting too many concurrent requests. Consider batching or using a semaphore to limit the number of concurrent requests:
public async Task<IEnumerable<Product>> GetProductsWithSemaphoreAsync(IEnumerable<int> productIds)
{
var client = _clientFactory.CreateClient("product-api");
var results = new List<Product>();
// Limit to a maximum of 5 concurrent requests
using var semaphore = new SemaphoreSlim(5);
var tasks = productIds.Select(async id =>
{
await semaphore.WaitAsync();
try
{
return await client.GetFromJsonAsync<Product>($"/api/products/{id}");
}
finally
{
semaphore.Release();
}
});
return await Task.WhenAll(tasks);
}
Proper use of <span>HttpClient</span><span> is crucial for creating high-performance, reliable, and maintainable applications. By adopting </span><code><span>HttpClientFactory</span><span> and following the best practices outlined in this article, you can avoid common pitfalls and build robust applications capable of effectively handling HTTP communication.</span>
Remember these key points:
- • Use
<span>HttpClientFactory</span>
instead of directly instantiating<span>HttpClient</span>
- • Use strongly typed clients
- • Set reasonable timeouts
- • Implement resilience patterns
- • Properly handle authentication and concurrency
- • Consider performance optimizations like compression
By following these practices, your applications will better handle the challenges of network communication and provide a better experience for users.
Recommended Reading:Basics of Chained Logging in Distributed Services – Understanding and Using DiagnosticSource and DiagnosticListenerDeepSeek API Client: A .NET Development Tool for Easy Access to DeepSeek AI ModelsDeep Dive into .NET 9: Six Core Upgrades from the Perspective of Senior DevelopersA Powerful Open Source Network Management and Troubleshooting Tool Based on .NET!example-voting-app: An Excellent Example for Learning Containerized Application Development and Operations.Looking Ahead to .NET 10 and C# 13: A Preview of New Features for 2025
Click the card below to follow DotNet NB
Let’s learn and communicate together
▲Click the card above to follow DotNet NB and learn together
Please reply in the public account backend
Reply【Roadmap】to get the .NET 2024 Developer RoadmapReply【Original Content】to get original content from the public accountReply【Summit Video】to get .NET Conf conference videosReply【Personal Profile】to get the author’s personal profileReply【Year-End Summary】to get the author’s year-end reviewReply【Add Group】Join the DotNet NB Learning and Communication Group
Long press to recognize the QR code below, or click to read the original text. Join me in learning, sharing insights.