Modern .NET applications rarely create a new network connection for every HTTP request. Instead, HttpClient relies on connection pooling so multiple requests can reuse established connections.
Connection reuse is essential for performance, but it introduces another problem: connections can become stale.
A backend service may change its IP address, a load balancer may rotate endpoints, a network path may change, or infrastructure may be replaced while an application continues using an existing connection. If the client keeps reusing connections indefinitely, it may not respond to infrastructure changes as quickly as expected.
.NET provides connection lifetime controls that allow applications to periodically replace pooled connections. Newer .NET networking capabilities make this area particularly relevant for applications that need predictable connection eviction behavior.
This article explains connection-pool staleness, how HTTP connection pooling works in .NET, how connection eviction can improve resilience, and how to test the behavior without confusing connection management with application-level failures.
Why HTTP Connection Pooling Exists
Creating a TCP connection has a cost.
Depending on the protocol and environment, establishing a connection can involve:
DNS resolution
TCP connection establishment
TLS negotiation
HTTP protocol negotiation
Request processing
Reusing an existing connection avoids repeating much of that work.
A simplified lifecycle looks like this:
Application
|
v
HttpClient
|
v
HttpClientHandler / SocketsHttpHandler
|
v
Connection Pool
|
+---- Connection A
+---- Connection B
+---- Connection C
|
v
Remote Service
When another request targets the same endpoint, the HTTP stack can reuse an existing connection from the pool.
This generally improves:
Latency
Throughput
CPU efficiency
TLS reuse
Resource utilization
However, connection reuse also means the client retains state about the remote endpoint.
What Is Connection Pool Staleness?
Connection-pool staleness occurs when an existing connection remains in the client pool longer than is desirable for the surrounding infrastructure.
Consider a service deployed behind DNS:
api.example.internal
|
v
DNS Resolver
|
+----+----+
| |
Server A Server B
10.0.0.10 10.0.0.20
The application initially connects to Server A.
Later, infrastructure changes:
Before:
api.example.internal
|
v
10.0.0.10
After:
api.example.internal
|
v
10.0.0.20
If an already-established connection remains healthy, there may be no immediate reason for the HTTP client to create a new connection.
That is normally desirable.
But in some architectures, you want connections to be periodically recycled so that DNS changes, infrastructure rotation, and load-balancing changes can eventually take effect.
Connection Lifetime and Connection Eviction
.NET's HTTP stack exposes connection lifetime controls through SocketsHttpHandler.
One important setting is:
PooledConnectionLifetime
It specifies how long a connection can remain in the connection pool before it becomes eligible for replacement.
For example:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
};
using var client = new HttpClient(handler);
This does not mean the connection is forcibly terminated exactly at five minutes while a request is actively using it.
Instead, the lifetime becomes a criterion for determining whether an existing pooled connection should continue to be reused.
Microsoft's HTTP client guidance specifically recommends connection lifetime configuration for scenarios where DNS changes need to be respected while still retaining the benefits of connection pooling.
Why DNS Makes This Important
DNS is often treated as if it were a one-time lookup.
In production systems, that assumption is dangerous.
Cloud infrastructure frequently uses DNS for:
Service discovery
Load balancing
Failover
Blue-green deployments
Infrastructure replacement
Regional routing
Suppose a client resolves:
payments.internal
↓
10.10.1.15
Later, the service moves:
payments.internal
↓
10.10.2.20
A new DNS lookup can discover the new address.
But an existing pooled connection doesn't need DNS because it already has an established network connection.
This is why connection lifetime and DNS behavior need to be considered together.
HttpClient Does Not Automatically Mean One Connection
Another common misconception is:
One
HttpClientequals one TCP connection.
That is not correct.
A single HttpClient can use multiple connections depending on the destination, protocol, concurrency, and handler configuration.
For example:
HttpClient
|
+-- api.example.com
| +-- Connection 1
| +-- Connection 2
|
+-- auth.example.com
+-- Connection 3
The handler manages the underlying connection pools.
This is why creating HttpClient instances repeatedly is usually not the solution to connection-management problems.
The Recommended HttpClient Pattern
For long-running applications, prefer a long-lived client or use IHttpClientFactory.
A simple configuration is:
builder.Services.AddHttpClient("Payments", client =>
{
client.BaseAddress = new Uri("https://payments.example.com");
});
Then consume the client through dependency injection:
public sealed class PaymentService
{
private readonly HttpClient _client;
public PaymentService(IHttpClientFactory factory)
{
_client = factory.CreateClient("Payments");
}
public async Task<string> GetPaymentStatusAsync(
string paymentId,
CancellationToken cancellationToken)
{
using var response = await _client.GetAsync(
$"/payments/{paymentId}",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(
cancellationToken);
}
}
IHttpClientFactory provides centralized configuration and handler management, while the underlying HTTP infrastructure still handles connection pooling.
Configuring Connection Eviction with IHttpClientFactory
If the application needs explicit connection lifetime management, configure the underlying handler:
builder.Services
.AddHttpClient("Payments")
.ConfigurePrimaryHttpMessageHandler(() =>
new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
});
The important design decision is the lifetime value.
There is no universal value that works for every application.
For example:
| Environment | Potential Concern |
|---|---|
| Stable internal service | Long connection lifetime may be acceptable |
| Frequently changing infrastructure | Shorter lifetime may be useful |
| DNS-based failover | Connection recycling can help |
| High-throughput service | Excessive recycling can increase connection overhead |
| Mobile or unstable networks | Connection behavior requires workload-specific testing |
The correct value should come from infrastructure behavior and measurements.
Connection Lifetime vs Idle Timeout
These settings solve different problems.
PooledConnectionLifetime
Controls how long a pooled connection is considered usable before replacement.
PooledConnectionLifetime = TimeSpan.FromMinutes(5);
PooledConnectionIdleTimeout
Controls how long an idle connection can remain in the pool.
Join the conversation! Your thoughts help the community grow.