Skip to content
Summary by AI

Stop Using HttpClient Wrong! Mistakes that can Crash Your .NET Apps

When working with HttpClient in ASP.NET Core, developers often fall into common traps that can lead to poor performance, resource exhaustion, and unreliable applications. These aren't just theoretical problems—they're the kind of issues ...

Http client Anti Patterns
00:00 / --:--

Read by your browser's built-in voice.

Key Takeaways

Improper HttpClient management in ASP.NET Core can cause critical issues like memory inefficiency, socket exhaustion, and DNS staleness. Using IHttpClientFactory along with modern .NET features ensures reliable, performant, and maintainable HTTP communication.

  • Avoid EnsureSuccessStatusCode() for business logic, as it throws unnecessary exceptions for non-2xx status codes.
  • Utilize ReadFromJsonAsync to deserialize directly from the stream, reducing memory allocations compared to reading strings.
  • Prevent socket exhaustion by replacing manual HttpClient instantiation with IHttpClientFactory for proper handler pooling.
  • Do not use static HttpClient instances, which fail to rotate connections and handle DNS changes in dynamic environments.
  • Avoid modifying client properties per request; instead, configure them in Program.cs and use HttpRequestMessage for specific request customizations.

When working with HttpClient in ASP.NET Core, developers often fall into common traps that can lead to poor performance, resource exhaustion, and unreliable applications. These aren't just theoretical problems—they're the kind of issues that bring down production systems at 2 AM.

In this post, we'll explore five critical anti-patterns and their recommended solutions. Trust me, if you're using HttpClient, you're probably making at least one of these mistakes right now.

Anti-Pattern #1: Not Handling HTTP Errors Properly

❌ The Problem

var response = await httpClient.GetAsync("posts/1");
response.EnsureSuccessStatusCode(); // Throws exception on non-success status codes

Why it's bad:

  • EnsureSuccessStatusCode() throws an HttpRequestException for any non-2xx status code

  • This treats business logic errors (like 404 Not Found) the same as infrastructure failures

  • You lose the ability to handle different HTTP status codes differently

  • Exception handling is expensive and should be reserved for exceptional cases

✅ The Solution

var response = await httpClient.GetAsync("posts/1");

if (response.IsSuccessStatusCode)
{
    var post = await response.Content.ReadFromJsonAsync<JsonElement>();
    // Process successful response
}
else
{
    // Handle different status codes appropriately
    switch (response.StatusCode)
    {
        case System.Net.HttpStatusCode.NotFound:
            // Handle 404
            break;
        case System.Net.HttpStatusCode.Unauthorized:
            // Handle 401
            break;
        default:
            // Handle other errors
            break;
    }
}

✅ Benefits:

  • Better control over error handling

  • Ability to differentiate between different HTTP status codes

  • More efficient than exception-based control flow

  • Improved user experience with specific error messages

Anti-Pattern #2: Reading Response Content Twice

❌ The Problem

var content = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<JsonElement>(content);

Why it's bad:

  • Creates an unnecessary intermediate string allocation

  • Wastes memory by holding the entire response as a string

  • Additional deserialization step is redundant

  • Poor performance, especially with large payloads

⚠️ Performance Impact

For a 1MB JSON response, you're essentially doubling your memory usage by creating both a string copy AND the deserialized object. This adds up quickly under load!

✅ The Solution

var post = await response.Content.ReadFromJsonAsync<JsonElement>();

✅ Benefits:

  • Direct deserialization from the HTTP stream

  • Reduced memory allocations

  • Better performance

  • Cleaner, more readable code

  • Built-in support in .NET 5+

Anti-Pattern #3: Creating New HttpClient Instances (Socket Exhaustion)

This is the big one. The mistake that literally brings down production systems.

❌ The Problem

using (var httpClient = new HttpClient())
{
    var response = await httpClient.GetAsync("https://api.example.com/posts/1");
    // Process response
}

Why it's bad:

  • Each HttpClient instance creates new TCP connections

  • Connections aren't immediately released when HttpClient is disposed

  • Leads to socket exhaustion under load

  • TIME_WAIT state can keep sockets occupied for minutes

  • Can cause "Cannot assign requested address" errors in production

💥 Real-world impact:

  • Under high load, your application can run out of available sockets

  • Even though you're using using statements, connections remain in TIME_WAIT state

  • Can bring down your entire application

🤔 The Initial Solution (That Creates Another Problem)

You might think: "I'll just use a static HttpClient to reuse the same instance!"

private static readonly HttpClient _httpClient = new HttpClient();

This does solve the socket exhaustion issue, but it introduces a new problem: DNS staleness (see Anti-Pattern #4 below).

✅ The Real Solution: IHttpClientFactory

// In Program.cs
builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

// In Controller
public class HomeController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public HomeController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task<IActionResult> GetData()
    {
        var httpClient = _httpClientFactory.CreateClient("MyAPI");
        var response = await httpClient.GetAsync("posts/1");
        // Process response
    }
}

✅ Benefits of IHttpClientFactory:

  • Manages HttpClient lifetime properly

  • Pools HttpMessageHandler instances to avoid socket exhaustion

  • Automatically handles DNS changes (rotates handlers every 2 minutes by default)

  • Supports named and typed clients

  • Integrates with Polly for resilience policies

  • Enables centralized configuration

Anti-Pattern #4: Using Static HttpClient Without Consideration

❌ The Problem (From Anti-Pattern #3's "Solution")

private static readonly HttpClient _staticHttpClient = new HttpClient();

While this solves socket exhaustion, it creates new issues:

Why it's problematic:

  • Static HttpClient doesn't respect DNS changes

  • The underlying HttpMessageHandler caches DNS results indefinitely

  • Can cause issues in cloud environments with load balancers

  • If a service IP changes, your app might keep using the old (stale) IP for the lifetime of the application

  • No built-in support for resilience patterns

  • In Kubernetes or container environments, this can route traffic to terminated pods

🌐 Real-world scenario

Imagine your API runs on Azure with auto-scaling. When instances scale up/down, DNS entries change. A static HttpClient will continue using the old IP addresses, causing failures even though the service is healthy!

When it might be acceptable:

  • Simple applications with a single HTTP endpoint that never changes

  • Services that don't change their DNS

  • Non-production/demo scenarios

  • Very simple console applications

✅ The Solution

Use IHttpClientFactory as shown in Anti-Pattern #3. It gives you the best of both worlds:

✅ Reuses connections (avoids socket exhaustion)

✅ Respects DNS changes (rotates handlers periodically)

✅ Provides resilience patterns

✅ Centralized configuration

Anti-Pattern #5: Setting HttpClient Properties Per Request

❌ The Problem

var httpClient = _httpClientFactory.CreateClient("MyAPI");
httpClient.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
httpClient.Timeout = TimeSpan.FromSeconds(30);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");

Why it's bad:

  • IHttpClientFactory returns clients from a pool

  • Modifying default properties affects the pooled instance

  • Can cause race conditions and unpredictable behavior

  • Headers accumulate if the same instance is reused

  • Thread-safety issues in concurrent scenarios

🐛 The Bug You'll Chase for Hours

Request A sets a header. Request B gets the same pooled client and sees Request A's header. Request B adds another header. Request C gets duplicate headers. Welcome to debugging hell!

✅ The Solution

Configure named clients in Program.cs:

builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

For per-request headers, use HttpRequestMessage:

var httpClient = _httpClientFactory.CreateClient("MyAPI");
var request = new HttpRequestMessage(HttpMethod.Get, "posts/1");
request.Headers.Add("Authorization", $"Bearer {token}");
var response = await httpClient.SendAsync(request);

Complete Example: Best Practices Implementation

Here's a complete example showing all the fixes applied:

Program.cs Configuration

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// Named client configuration
builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

// Typed client configuration (recommended)
builder.Services.AddHttpClient<IPostService, PostService>(client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

var app = builder.Build();
// ... rest of configuration

Controller with Best Practices

public class HomeController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public HomeController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task<IActionResult> BestPractice()
    {
        try
        {
            var httpClient = _httpClientFactory.CreateClient("MyAPI");
            var response = await httpClient.GetAsync("posts/1");
            
            if (response.IsSuccessStatusCode)
            {
                var post = await response.Content.ReadFromJsonAsync<JsonElement>();
                ViewBag.Method = "Best Practice";
                ViewBag.Title = post.GetProperty("title").GetString();
                ViewBag.Body = post.GetProperty("body").GetString();
            }
            else
            {
                ViewBag.Error = $"HTTP Error: {response.StatusCode}";
            }
        }
        catch (HttpRequestException httpEx)
        {
            // Handle network-level errors
            ViewBag.Error = $"Network Error: {httpEx.Message}";
        }
        catch (TaskCanceledException)
        {
            // Handle timeout
            ViewBag.Error = "Request timed out";
        }
        catch (Exception ex)
        {
            // Handle unexpected errors
            ViewBag.Error = $"Error: {ex.Message}";
        }
        
        return View("Index");
    }
}

Summary: Quick Reference

Anti-Pattern

Problem

Solution

#1: EnsureSuccessStatusCode()

Throws exceptions for all non-2xx responses

Check IsSuccessStatusCode and handle status codes explicitly

#2: ReadAsStringAsync + Deserialize

Unnecessary memory allocation and double processing

Use ReadFromJsonAsync<T>() directly

#3: new HttpClient() in using

Socket exhaustion under load

Use IHttpClientFactory

#4: Static HttpClient

DNS staleness, no resilience support

Use IHttpClientFactory

#5: Setting properties per request

Race conditions, unpredictable behavior

Configure in Program.cs, use HttpRequestMessage for per-request customization

Conclusion

Proper HttpClient usage is critical for building scalable, reliable ASP.NET Core applications. By avoiding these common anti-patterns and following the recommended practices:

  • Your application will handle more concurrent requests

  • You'll avoid socket exhaustion issues

  • Error handling will be more robust and user-friendly

  • Performance will improve through better memory management

  • Your code will be more maintainable and testable

💡 Remember:

Always use IHttpClientFactory in ASP.NET Core applications. It's the recommended approach that handles all the complexity of HttpClient lifetime management for you.

Now go forth and make HTTP requests the right way! Your production servers will thank you. 🚀

Rate this article

Rate this article out of 5

No ratings yet
Be the first to rate this article.

Selecting a star will ask you to sign in.
HV
Huy Vũ
Contributor at Selectgo

Shares practical engineering notes on this blog.