A small .NET service can look healthy in a quiet test. Then it can fail on a busy morning because it opens a new network client for every call. The failure is often a shortage of TCP ports, not a bug in the ERP API it is calling.

Some quick terms first. HttpClient is the .NET class that sends HTTP requests and reads the replies. HTTP is the protocol used by most web and REST APIs. A connection pool is a set of open network connections that a client can reuse. TCP is the transport that carries those connections. After a connection closes, the operating system keeps its port in a short "TIME-WAIT" state, so the port cannot be used again at once. Domain Name System, or DNS, is the directory that maps a name such as api.example.com to a network address.

This write-up is from Aimpex Resources Group LLC. Our Lincoln, NE team works on ERP Architecture across SAP S/4HANA, Oracle Cloud ERP and Microsoft Dynamics. We place .NET engineers on the integrations that pull that data into other tools. A fresh HttpClient on every request is one of the first things worth looking for in those services.

What does the problem look like?

A team built a .NET service to pull purchase orders from an ERP REST API. The method that called the API created a new HttpClient inside a using block, sent one request, and disposed the client. In a short test this looked fine. After a long run of calls, new requests began to fail, and the machine showed a large number of TCP connections sitting in TIME-WAIT.

The shape of the code was like this:

public async Task<string> ReadOrdersAsync(Uri url) { using var client = new HttpClient(); return await client.GetStringAsync(url); } 

Each call built a new client, and each client had its own connection pool. Disposing the client closed the connections, and the ports then sat in TIME-WAIT.

Why does a new client on every call run out of ports?

Microsoft's page on using HttpClient (last updated August 6, 2026) states the cause directly. "The connection pool for an HttpClient is linked to the underlying SocketsHttpHandler." A new client means a new pool. "Sending another request to the same server using a different handler requires a new connection." After a connection closes, "TCP ports aren't released immediately." Microsoft points to the TIME-WAIT behavior in RFC 9293. "If the rate of requests is high, the operating system limit of available ports might be exhausted."

The official advice that follows is short: "To avoid port exhaustion problems, reuse HttpClient instances for as many HTTP requests as possible."

There is a second trap if you reuse a client and never replace its connections. "HttpClient only resolves DNS entries when a connection is created. It doesn't track any time to live (TTL) durations specified by the DNS server." If the server's address changes, a client that keeps the old connection forever will still aim at the old address.

What does Microsoft recommend?

The same page sums up two safe lifetime patterns. "You should use either long-lived clients and set PooledConnectionLifetime (.NET Core and .NET 5+) or short-lived clients created by IHttpClientFactory."

For .NET Core and .NET 5 or later, Microsoft's first pattern is a static or singleton HttpClient with PooledConnectionLifetime set to an interval you choose. Microsoft's examples use two or 15 minutes. The 15-minute figure in Microsoft's sample "was chosen arbitrarily for illustration purposes." You should pick a value that matches how often you expect DNS or network changes. After that interval, and after the connection's last request finishes, the connection is closed. The next request creates a new one, and DNS is read again. This "solves both the port exhaustion and DNS changes problems" without adding IHttpClientFactory.

Microsoft also says that a limited number of long-lived clients is acceptable. "What matters is that they're not created and disposed with each request, as they each contain a connection pool."

On .NET Framework, Microsoft says to use IHttpClientFactory to manage HttpClient instances. "If you don't use the factory and instead create a new client instance for each request yourself, you can exhaust available ports."

What does the long-lived fix look like in C#?

This follows Microsoft's sample: one shared client, one handler, and a pooled-connection lifetime.

static readonly HttpClient SharedClient = new HttpClient( new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(15) }); public async Task<string> ReadOrdersAsync(Uri url) { return await SharedClient.GetStringAsync(url); } 

The client is created once and reused. Do not wrap SharedClient in a using statement on every call. Disposing that shared client would close the pool you are trying to keep.

If the service talks to more than one host, or needs different settings, Microsoft says more than one long-lived instance is fine. The mistake is creating and disposing a client per request, not having two or three shared ones.

When should you use IHttpClientFactory instead?

IHttpClientFactory is a helper in the Microsoft.Extensions.Http package. It creates HttpClient instances and reuses the handlers behind them. Microsoft's IHttpClientFactory page (also last updated August 6, 2026) says the factory-created clients are meant to be short-lived, and that the factory "manages the caching and lifetime of underlying HttpClientHandler instances." Microsoft's page on using HttpClient says that this reuse "avoids any socket exhaustion issues." It also avoids the DNS problem that shows up when a single client is kept forever.

A typical ASP.NET Core registration looks like this:

builder.Services.AddHttpClient(); 

A service then asks for IHttpClientFactory and creates a client when it needs one:

public sealed class OrderReader(IHttpClientFactory httpClientFactory) { public async Task<string> ReadOrdersAsync(Uri url) { HttpClient client = httpClientFactory.CreateClient(); return await client.GetStringAsync(url); } } 

This is safe because the factory, not the calling code, owns the handler and the pool. Creating a client from the factory on each call is the opposite of new HttpClient() on each call.

Microsoft warns that if the app relies on cookies, IHttpClientFactory is a poor fit. Pooled handlers share a CookieContainer, so cookies can leak between parts of the app, and cookies are lost when the handler is recycled. In that case, prefer the long-lived HttpClient with PooledConnectionLifetime.

How do you test it before production?

Pick a call you can repeat against a test endpoint, and run it in a tight loop. First try a new HttpClient on every call, and then try the shared client or the factory.

  • Watch the number of TCP connections in TIME-WAIT on the machine, for example with netstat or Get-NetTCPConnection in PowerShell. The per-request client should leave a growing pile of waiting ports. The shared or factory client should not.
  • Confirm that later requests still succeed after several minutes, so you have passed at least one PooledConnectionLifetime interval.
  • If you use the factory and the API depends on cookies, confirm that cookies are not crossing from one user or one flow into another.

The ERP API was not the broken part. The client that called it was. Reuse a long-lived HttpClient with PooledConnectionLifetime, or let IHttpClientFactory manage short-lived clients, exactly as Microsoft describes.

Sources

Aimpex Resources Group LLC is a Lincoln, NE firm founded in 2025 that works in ERP Architecture, Supply Chain Analytics and IT Staffing Solutions. We operate as a Direct Vendor for Bank of America. We are a Verified Employer with active corporate sponsorship pathways for qualified STEM OPT and H-1B professionals. Our ERP architects work across SAP S/4HANA, Oracle Cloud ERP and Microsoft Dynamics. Our staffing practice aligns SAP specialists, Java and .NET engineers, cloud practitioners, data professionals and quality engineers with clearly defined roles. Teams that want cleaner .NET clients on those integrations can learn more at aimpexresources.com. Sponsorship is never a promise of visa selection or approval.