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.
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2);
They can be combined:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
};
Conceptually:
Connection created
|
+----------------------+
| |
v v
Active requests Idle period
| |
| Idle timeout
| |
v v
Lifetime policy Remove connection
|
v
Connection replacement
The distinction is important when diagnosing connection-pool behavior.
The Cost of Aggressive Eviction
Connection eviction is not free.
If connections are recycled too frequently, the application may repeatedly perform:
TCP handshake
+
TLS handshake
+
Connection setup
That can increase:
Connection establishment latency
CPU usage
TLS overhead
Network overhead
Server connection churn
For example, a five-second lifetime may sound attractive for quickly detecting infrastructure changes, but it could be wasteful for a high-throughput service whose backend remains stable.
The objective is therefore not:
Recycle connections as quickly as possible
It is:
Recycle connections often enough to satisfy
infrastructure freshness requirements without
creating unnecessary connection churn.
Testing Connection Eviction
A useful test should demonstrate that connections are actually being replaced.
Configure a deliberately short lifetime in a test environment:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(10)
};
using var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://localhost:5001")
};
Then issue repeated requests:
for (var i = 0; i < 100; i++)
{
var response = await client.GetAsync("/health");
response.EnsureSuccessStatusCode();
await Task.Delay(1_000);
}
The test should observe connection behavior rather than simply checking HTTP status codes.
Useful telemetry includes:
Request latency
DNS resolution behavior
Connection creation
Connection closure
TLS negotiation
HTTP status codes
Exceptions
Connection counts
Testing DNS Failover
A stronger experiment uses two endpoints.
Start with:
service.test
↓
Server A
Then switch the DNS configuration:
service.test
↓
Server B
Run a long-lived client throughout the experiment.
Compare two configurations:
Scenario A
Long-lived pooled connections
Scenario B
Configured PooledConnectionLifetime
The objective is to observe how quickly new connections begin targeting the new endpoint.
Do not treat the exact transition time as a guaranteed DNS propagation time. DNS caching can exist at multiple layers, and connection reuse is only one part of the overall resolution path.
Connection Pool Staleness vs DNS Caching
These are related but different problems.
| Problem | Description | Possible Control |
|---|---|---|
| DNS cache | Previously resolved hostname remains cached | DNS/handler configuration |
| Existing connection | Active connection continues to be reused | Connection lifetime |
| Idle connection | Unused connection remains available | Idle timeout |
| Server-side closure | Remote server terminates connection | HTTP retry/resilience handling |
| Network failure | Connection becomes unusable | Retry/failover strategy |
Changing PooledConnectionLifetime does not solve every DNS or networking problem.
Production Resilience Requires More Than Eviction
Connection eviction should not be treated as a failover mechanism by itself.
A resilient HTTP client may also need:
Request timeouts
Cancellation tokens
Retry policies for appropriate transient failures
Circuit breaking
Observability
DNS-aware infrastructure
Load-balancer health checks
For example:
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(10));
var response = await client.GetAsync(
"/payments",
cts.Token);
Timeouts prevent a request from remaining blocked indefinitely.
Retries, meanwhile, should be applied selectively. Retrying every failure can amplify outages and create additional load on an already unhealthy service.
Common Mistakes
Creating a New HttpClient for Every Request
This can prevent effective connection reuse and contribute to unnecessary resource consumption.
Prefer dependency injection or a properly managed long-lived client.
Setting an Extremely Short Connection Lifetime
Aggressive eviction may create more overhead than it solves.
Measure connection creation and request performance before selecting a value.
Assuming Connection Lifetime Controls DNS TTL
It does not.
DNS caching and connection reuse are separate layers.
Using Connection Eviction as Failover
Eviction helps refresh connections. It does not replace health checks, retry policies, service discovery, or load balancing.
Testing Only With localhost
Localhost does not reproduce many real networking conditions.
For meaningful resilience testing, introduce realistic DNS, latency, load-balancing, and deployment behavior where possible.
Troubleshooting
Requests Continue Reaching the Old Server
Check:
DNS cache behavior.
Existing connection reuse.
Connection lifetime configuration.
Proxy or load-balancer behavior.
Whether the old endpoint is still reachable.
Whether the test actually triggered creation of a new connection.
Latency Increased After Reducing Connection Lifetime
Look for increased connection establishment and TLS negotiation.
A shorter lifetime can increase connection churn.
Connections Are Closed Unexpectedly
Inspect both client and server logs.
The remote service may have its own idle timeout or maximum connection lifetime.
DNS Changes Are Not Reflected Immediately
Do not assume the application is the only caching layer.
The complete path can include:
Application
↓
HTTP Handler
↓
OS / DNS Resolver
↓
DNS Infrastructure
↓
Load Balancer
↓
Service
Each layer can influence observed behavior.
Best Practices
Reuse
HttpClientrather than creating one per request.Use
IHttpClientFactoryfor centrally managed clients.Configure connection lifetime based on infrastructure requirements.
Distinguish connection lifetime from idle timeout.
Test DNS changes with long-running clients.
Measure connection creation and TLS overhead.
Avoid unnecessarily aggressive connection eviction.
Combine connection management with proper request timeouts.
Use retries only for failures that are safe to retry.
Monitor connection and request telemetry in production.
Frequently Asked Questions
Does PooledConnectionLifetime force-close every connection at the configured time?
Not necessarily. It establishes a lifetime policy for pooled connections. Active requests and the connection-pool lifecycle affect when a connection is actually removed and replaced.
Does connection eviction fix DNS failover?
It can help the client establish new connections after an endpoint changes, but DNS caching and infrastructure behavior still determine when the new address becomes visible.
Should I use a very short connection lifetime for cloud applications?
Not by default. Cloud environments can benefit from connection recycling, but excessive recycling creates additional connection and TLS overhead.
Is connection pooling bad for microservices?
No. Connection pooling is generally essential for efficient HTTP communication between services. The important question is how connection lifetime should be managed.
Is IHttpClientFactory a replacement for connection lifetime configuration?
No. IHttpClientFactory helps manage and configure HTTP clients and handlers. Connection lifetime remains an underlying networking concern that can be configured when the application requires it.
Conclusion
HTTP connection pooling is one of the reasons modern .NET applications can communicate efficiently with remote services. Reusing connections avoids repeated network setup and can significantly improve request performance.
The same reuse can become problematic when infrastructure changes faster than the lifetime of the pooled connections.
Connection lifetime controls provide a way to balance these competing requirements. By periodically making pooled connections eligible for replacement, applications can reduce the risk of holding onto connections longer than the surrounding infrastructure expects.
The key is to treat connection eviction as part of a broader networking strategy. Measure DNS behavior, connection reuse, TLS overhead, latency, and failure recovery together. A well-designed connection policy should provide enough freshness for the application's infrastructure without turning every request into a new connection-establishment operation.

Jasen FiciPosted Aug 17, 2026, 11:38 AM
Thanks for sharing this. We featured it in DotNetNews here: https://dotnetnews.co/archive/the-net-news-daily-issue-520/