DEV Community

Cover image for The HttpClient Mistake That Can Slow Down Your .NET API
qodors
qodors

Posted on Originally published at linkedin.com

The HttpClient Mistake That Can Slow Down Your .NET API

An API can run smoothly during development and still struggle when traffic increases. Requests that take milliseconds locally may become much slower in production. Before investigating the database or server resources, it is worth checking how the application makes calls to external services.

One common issue is creating a new HttpClient for every outgoing request.

ASP.NET Core applications use HttpClient to communicate with payment gateways, authentication providers, third-party APIs, and other backend services. Creating and disposing of a client inside a frequently called method may seem reasonable, but it can prevent efficient connection reuse and contribute to socket exhaustion under heavy load.

Let's look at why this happens and how to manage HTTP connections properly in .NET.

THE HTTPCLIENT MISTAKE
Consider a method that retrieves customer details from another service:

public async Task<Customer?> GetCustomerAsync(int id)
{
   using var client = new HttpClient();
   var response = await client.GetAsync(
        $"https://customer-api/customers/{id}");
     response.EnsureSuccessStatusCode();
     return await response.Content
         .ReadFromJsonAsync<Customer>();
}
Enter fullscreen mode Exit fullscreen mode

The method is easy to follow. It creates a client, sends a request, reads the response, and disposes of the client.

The problem is doing this every time the method runs.

Each HttpClient instance uses an underlying handler that manages its connection pool. When the application repeatedly creates clients with new handlers, it also creates and discards connection pools instead of reusing them effectively.

Under heavy traffic, this can increase connection overhead and contribute to socket exhaustion. Connections may remain in states such as TIME_WAIT after they are closed, adding pressure to available network resources.

Depending on the traffic and connection behavior, the application may experience slower requests or failed outbound calls.

WHY CREATING A CLIENT FOR EVERY REQUEST CAN BE A PROBLEM
Opening and managing network connections takes resources. Reusing connections helps reduce that overhead.

Imagine an e-commerce API that calls a payment service every time a customer checks out. If each checkout creates a new HttpClient with its own handler, the application may repeatedly establish connections instead of making better use of existing ones.

As traffic grows, this connection churn can become a performance issue.

That does not mean every short-lived HttpClient will immediately cause socket exhaustion. The impact depends on request volume, connection behavior, operating system resources, and how the application handles outgoing requests.

The key is to manage the underlying handlers and connection pools instead of creating a new client-handler pair for every request.

THE RECOMMENDED SOLUTION: IHTTPCLIENTFACTORY
ASP.NET Core provides IHttpClientFactory to create configured clients and manage their underlying handler lifetimes.

First, register a typed client in Program.cs:

builder.Services
    .AddHttpClient<CustomerService>(client =>
    {
      client.BaseAddress =
           new Uri("https://customer-api/");
    });
Enter fullscreen mode Exit fullscreen mode

Then inject the HttpClient into the service:

public class CustomerService
{
   private readonly HttpClient _httpClient;
   public CustomerService(HttpClient httpClient)
   {
      _httpClient = httpClient;
   }
   public async Task<Customer?> GetCustomerAsync(
       int id,
       CancellationToken cancellationToken = default)
   {
       using var response = await _httpClient.GetAsync(
            $"customers/{id}",
            cancellationToken);
       response.EnsureSuccessStatusCode();
       return await response.Content
           .ReadFromJsonAsync<Customer>(
                cancellationToken);
   }
}
Enter fullscreen mode Exit fullscreen mode

The factory manages the underlying handlers, allowing their connection pools to be reused across client instances. It also makes it easier to configure different external services, add logging, and integrate resilience strategies.

This approach avoids creating a new handler and connection pool every time the service sends a request.

WHAT ABOUT A SINGLETON HTTPCLIENT?
IHttpClientFactory is not the only option. A long-lived HttpClient can also reuse its connection pool effectively.

For example, an application can create a client using a configured handler:

var handler = new SocketsHttpHandler
{
   PooledConnectionLifetime =
       TimeSpan.FromMinutes(5)
};
var client = new HttpClient(handler)
{
   BaseAddress = new Uri("https://customer-api/")
};
Enter fullscreen mode Exit fullscreen mode

The five-minute lifetime is only an example. The appropriate value depends on the application's DNS requirements and infrastructure.

Why does this setting matter? A long-lived client does not automatically recreate its connections every time DNS records change. Setting PooledConnectionLifetime allows connections to be replaced after a configured period, helping the client pick up DNS changes when new connections are established.

Keep the client alive and reuse it rather than creating a new instance for every request.

For many ASP.NET Core applications, IHttpClientFactory is a convenient default because it works naturally with dependency injection and manages handler lifetimes. A long-lived client is also a valid choice when configured for the application's needs.

COMMON HTTPCLIENT MISTAKES TO AVOID
Correct client lifetime management is important, but it is not the only thing to consider when calling external services.

  • Handle unsuccessful responses: Use EnsureSuccessStatusCode() or check status codes explicitly.

  • Set timeouts: Choose reasonable limits so requests do not wait longer than the application can tolerate.

  • Pass cancellation tokens: Allow HTTP calls and response-reading operations to stop when cancellation is requested.

  • Use retries carefully: Retry transient failures with an appropriate resilience strategy. Consider the effects before retrying non-idempotent operations.

  • Reuse connections: Use IHttpClientFactory or a properly configured long-lived client.

  • Monitor outgoing requests: Track latency, timeouts, failures, and connection-related issues.

  • Configure clients by service: Use named or typed clients when different APIs need different base addresses, headers, or other settings.

These steps help you identify problems and handle failures without adding unnecessary load.

TESTING HTTPCLIENT PERFORMANCE
A connection-management issue may be difficult to notice during local development, where request volume is usually lower.

Test the application under realistic traffic and monitor:

  • Outbound HTTP request duration.
  • Request failures and timeout rates.
  • Socket and connection behavior.
  • CPU and memory usage.
  • Performance under concurrent requests.
  • Response times from external services.

If the API slows down under load, check its outgoing HTTP calls before assuming the database is responsible.

Compare measurements before and after changing the client lifetime strategy. This helps confirm whether connection management was actually contributing to the problem.

OUR TAKE
When building backend systems at Qodors, details like HTTP connection management deserve attention because they can affect how an application behaves under load.

The important distinction is between creating a client object and managing the handler and connection pool behind it. Understanding that difference helps developers choose an approach that fits their application.

For most ASP.NET Core projects, IHttpClientFactory is a practical starting point. A long-lived client can work just as well when connection lifetimes are configured appropriately.

Whichever approach you choose, test it under realistic traffic and monitor outbound requests. That is a more reliable way to find performance issues than guessing at the cause.

QUICK REFERENCE
Avoid creating a new HttpClient with a new handler for every request.
Use IHttpClientFactory to manage handler lifetimes.
Consider a long-lived client with PooledConnectionLifetime when appropriate.
Configure timeouts and pass cancellation tokens.
Handle HTTP errors and retries carefully.
Monitor outbound request latency and failures.
Test connection behavior under realistic traffic.

An API's performance depends partly on how efficiently it communicates with the services it relies on.

DotNet #ASP.NETCore #HttpClient #IHttpClientFactory #DotNetDevelopment #APIPerformance #BackendDevelopment #SoftwareEngineering #WebDevelopment #PerformanceOptimization #QodorsEdge

Written by the team at Qodors — practical insights on .NET, ASP.NET Core, and building reliable backend systems.
→ https://www.qodors.com/?utm_source=devto&utm_medium=post&utm_campaign=httpclient_performance

Top comments (0)