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:

  1. DNS resolution

  2. TCP connection establishment

  3. TLS negotiation

  4. HTTP protocol negotiation

  5. 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:

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:

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 HttpClient equals 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:

EnvironmentPotential Concern
Stable internal serviceLong connection lifetime may be acceptable
Frequently changing infrastructureShorter lifetime may be useful
DNS-based failoverConnection recycling can help
High-throughput serviceExcessive recycling can increase connection overhead
Mobile or unstable networksConnection 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:

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:

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.

ProblemDescriptionPossible Control
DNS cachePreviously resolved hostname remains cachedDNS/handler configuration
Existing connectionActive connection continues to be reusedConnection lifetime
Idle connectionUnused connection remains availableIdle timeout
Server-side closureRemote server terminates connectionHTTP retry/resilience handling
Network failureConnection becomes unusableRetry/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:

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:

  1. DNS cache behavior.

  2. Existing connection reuse.

  3. Connection lifetime configuration.

  4. Proxy or load-balancer behavior.

  5. Whether the old endpoint is still reachable.

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

  1. Reuse HttpClient rather than creating one per request.

  2. Use IHttpClientFactory for centrally managed clients.

  3. Configure connection lifetime based on infrastructure requirements.

  4. Distinguish connection lifetime from idle timeout.

  5. Test DNS changes with long-running clients.

  6. Measure connection creation and TLS overhead.

  7. Avoid unnecessarily aggressive connection eviction.

  8. Combine connection management with proper request timeouts.

  9. Use retries only for failures that are safe to retry.

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