SALE! All courses are on the lowest price! Enroll Now
ASP.NET Core

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

Bhrugen Patel

Bhrugen Patel

Author

Published
January 15, 2026
10 min read
1 views
Learn about the most common HttpClient antipatterns in .NET that can cause socket exhaustion, DNS issues, and application crashes. Discover best practices and how to use IHttpClientFactory correctly.
RESTful Web API in .NET Core - The Beginners Guide (.NET 10)

RESTful Web API in .NET Core - The Beginners Guide (.NET 10)

Beginner course on RESTful API with ASP.NET Core Web API that will take you from basics of API and teach you how to consume it.

121 Videos 7hr
Share this article:

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 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. 🚀

Bhrugen Patel

About Bhrugen Patel

Expert .NET developer and educator with years of experience in building real-world applications and teaching developers around the world.

FREE .NET Hosting
YouTube
Subscribe