Introduction
Most ASP.NET Core applications make outbound HTTP calls at some point. They may call payment services, authentication APIs, internal microservices, cloud services, or third-party platforms.
For these applications, HttpClient is usually long-lived and relies on connection pooling. Reusing connections avoids the cost of establishing a new TCP connection for every request and is an important part of efficient HTTP communication.
But long-lived connections introduce another problem.
What happens when the service behind a hostname changes its IP address?
This can happen during:
Deployment
Failover
DNS-based load balancing
Infrastructure migration
Disaster recovery
Service scaling
If an application keeps using an existing connection for too long, changing DNS information does not necessarily mean that an already-established connection immediately moves to the new destination.
.NET 11 introduces configurable HTTP connection eviction capabilities that provide developers with more control over when pooled connections should be removed.
This article explains the problem, shows how to configure connection lifetime behavior, and demonstrates how to test DNS failover without claiming that a particular configuration is universally optimal.
Why DNS Changes Do Not Automatically Move Existing Connections
Consider an API client calling:
https://api.example.com
Initially, DNS resolves the hostname to:
api.example.com
|
v
10.0.0.10
Later, the service moves:
api.example.com
|
v
10.0.0.20
A new DNS lookup can discover the new address.
However, an application may already have an established HTTP connection to the previous destination.
Conceptually:
Existing connection
|
v
10.0.0.10
DNS now points to
|
v
10.0.0.20
The existing connection does not become a connection to the new IP simply because DNS changed.
That is where connection lifetime management becomes important.
How HttpClient Connection Pooling Works
A common pattern is to reuse a single HttpClient rather than creating a new instance for every request.
For example:
builder.Services.AddHttpClient<PaymentClient>(client =>
{
client.BaseAddress =
new Uri("https://payments.example.com");
});
The underlying HTTP handler maintains connections and can reuse them for subsequent requests.
This is generally preferable to repeatedly doing:
using var client = new HttpClient();
await client.GetAsync("https://payments.example.com");
Creating clients repeatedly can prevent effective connection reuse and can introduce unnecessary connection-management overhead.
The important point is that a long-lived client does not mean every request creates a new network connection.
That is normally a benefit, but it also means connection lifetime needs to be considered when infrastructure changes.
What Is Connection Eviction?
Connection eviction means removing a connection from the pool after a defined condition is reached.
One useful condition is age.
Conceptually:
Connection created
|
v
Connection reused
|
v
Connection reused
|
v
Maximum lifetime reached
|
v
Connection removed
|
v
New connection created
|
v
DNS can be resolved again
The exact behavior depends on the HTTP handler and configuration being used.
The purpose is not to force a new connection for every request.
It is to periodically give the application an opportunity to establish a fresh connection.
Why DNS Failover Testing Matters
Suppose an application calls a backend service continuously.
At 10:00:
api.internal
|
v
10.0.0.10
At 10:30, the backend fails and DNS is updated:
api.internal
|
v
10.0.0.20
If the application keeps using an existing connection to 10.0.0.10, requests can continue failing until that connection is closed or becomes unusable.
Connection eviction provides one mechanism for limiting how long such connections remain in the pool.
However, it is not a replacement for proper retry, timeout, health-check, and failover strategies.
Configuring HttpClient
A typical HttpClient registration looks like this:
builder.Services.AddHttpClient<BackendClient>(client =>
{
client.BaseAddress =
new Uri("https://api.example.com");
});
Connection lifetime settings are configured on the underlying HTTP handler.
For example, with SocketsHttpHandler:
builder.Services.AddHttpClient<BackendClient>()
.ConfigurePrimaryHttpMessageHandler(() =>
new SocketsHttpHandler
{
PooledConnectionLifetime =
TimeSpan.FromMinutes(5)
});
The lifetime shown here is an example configuration, not a universal recommendation.
The correct value depends on DNS TTLs, service behavior, infrastructure, traffic patterns, and the application's tolerance for reconnecting.
Choosing a Connection Lifetime
A common mistake is choosing an arbitrary lifetime such as five minutes and assuming that it guarantees reliable failover.
It does not.
The configuration should be based on the environment.
For example:
DNS changes every 10 minutes
|
v
Connection lifetime should be evaluated
against that infrastructure behavior
The goal is to balance two competing concerns.
Longer Connection Lifetime
Advantages:
More connection reuse
Fewer connection establishments
Less connection-management overhead
Potential drawback:
Existing connections can remain around longer
Shorter Connection Lifetime
Advantages:
Connections are refreshed more frequently
DNS changes can be discovered sooner through new connections
Potential drawback:
More connection establishments
Potentially higher connection-management overhead
There is no single value that is correct for every application.
Building a DNS Failover Test
A useful test environment should contain two backend endpoints.
For example:
Service A
10.0.0.10
Service B
10.0.0.20
The client uses:
api.test.local
Initially, DNS points to Service A.
The client sends repeated requests:
Request 1 -> Service A
Request 2 -> Service A
Request 3 -> Service A
Then change the DNS record to Service B.
The test should continue sending requests and record which backend receives them.
This allows you to observe how the client behaves as connections age out and new connections are established.
Recording the Backend
Make each backend return an identifier.
For example:
app.MapGet("/health", () =>
{
return Results.Ok(new
{
Server = Environment.GetEnvironmentVariable(
"SERVER_ID") ?? "unknown"
});
});
Run the same application twice with different environment variables:
SERVER_ID=backend-a
SERVER_ID=backend-b
The client can then log the response:
var response = await client.GetFromJsonAsync<BackendResponse>(
"/health");
Console.WriteLine(response?.Server);
This gives you a simple way to observe where requests are going.
Simulating DNS Changes
For a controlled local test, you can use a test DNS environment or host-level name resolution.
For example, a hosts file entry can be used for a simple local experiment:
127.0.0.1 api.test.local
For more realistic DNS behavior, use an actual DNS service or test environment where the record can be changed while the client remains active.
The important requirement is that the hostname stays the same while its destination changes.
If you simply change the URL from one server to another in the application, you are not testing DNS failover.
Testing Connection Eviction
Start with a long connection lifetime.
For example:
PooledConnectionLifetime =
TimeSpan.FromMinutes(30)
Start continuous requests:
Request -> backend A
Request -> backend A
Request -> backend A
Change DNS to backend B.
Continue the test.
Then repeat with a shorter lifetime:
PooledConnectionLifetime =
TimeSpan.FromMinutes(2)
The exact timing and observed behavior should come from the test environment.
The purpose is to compare how quickly new connections begin using the new destination.
Measuring Failover Behavior
Record at least these values:
| Metric | What It Tells You |
|---|---|
| DNS change time | When infrastructure changed |
| First new backend request | When new destination was observed |
| Failed requests | Impact during transition |
| Connection lifetime | Client configuration |
| Request interval | How frequently the client sends traffic |
| Recovery time | Time until normal behavior returns |
For example:
DNS changed
|
v
Requests continue
|
v
Existing connection reused
|
v
Connection evicted
|
v
New connection created
|
v
New DNS destination used
This makes the failover process easier to understand.
Connection Eviction Is Not a Failover Mechanism by Itself
This distinction is important.
Connection lifetime management can help an application discover infrastructure changes over time.
It does not guarantee that every request will succeed during a failure.
A production application should also consider:
Timeouts
Retries
Retry limits
Exponential backoff
Circuit breakers
Health checks
Load balancers
Service discovery
Idempotency
For example, a failed payment request should not simply be retried blindly.
Retry behavior depends on whether the operation is safe to repeat.
Adding Timeouts
An HTTP client should have an appropriate timeout.
For example:
builder.Services.AddHttpClient<BackendClient>(client =>
{
client.BaseAddress =
new Uri("https://api.example.com");
client.Timeout =
TimeSpan.FromSeconds(10);
});
The value should reflect the actual service contract.
A timeout that is too short can generate unnecessary failures.
A timeout that is too long can keep resources occupied during a backend outage.
Adding Retry Logic Carefully
Transient failures may justify retry behavior.
However, retries should be bounded.
Conceptually:
Request
|
v
Failure
|
v
Wait
|
v
Retry
|
v
Failure
|
v
Stop
Do not create an unlimited retry loop.
For write operations, verify that the operation is safe to repeat before automatically retrying it.
Common Mistakes
Assuming DNS Changes Existing Connections
DNS resolution and established connections are different things.
Changing a DNS record does not rewrite an already-open TCP connection.
Setting an Extremely Short Lifetime
A very short lifetime can increase connection establishment overhead.
The goal is controlled refresh, not constant reconnection.
Ignoring DNS TTL
Connection lifetime and DNS behavior should be considered together.
A client setting cannot compensate for an infrastructure design that is fundamentally inconsistent.
Testing by Changing the URL
Changing:
api-a.example.com
to:
api-b.example.com
does not test DNS failover.
The hostname must remain the same while its destination changes.
Relying Only on Connection Eviction
Connection eviction is one part of connection management.
It should not replace timeout and failure-handling strategies.
Troubleshooting
If the client does not appear to move to the new backend, check:
Whether the DNS record actually changed.
Whether the client is still using an existing connection.
Whether the configured lifetime has elapsed.
Whether a proxy is involved.
Whether a load balancer is maintaining its own connections.
Whether DNS caching exists elsewhere in the path.
Whether the backend is actually reachable.
Whether requests are being retried.
Whether the client is using HTTP/1.1 or HTTP/2.
Whether the test environment accurately represents production.
Network infrastructure can make this behavior more complicated than a simple DNS lookup.
HTTP/2 Considerations
Modern applications may use HTTP/2, where multiple requests can share a single connection.
That makes connection lifetime behavior particularly relevant because one connection can carry many concurrent requests.
The conceptual model becomes:
HTTP/2 connection
|
+--> Request 1
+--> Request 2
+--> Request 3
+--> Request 4
Closing or replacing that connection can affect multiple requests.
This is another reason not to choose connection lifetimes purely by guesswork.
Test the protocol and workload your application actually uses.
Production Considerations
Connection eviction should be considered together with the rest of the network architecture.
A typical production path might be:
Application
|
v
HttpClient
|
v
Proxy / Load Balancer
|
v
Service Discovery / DNS
|
v
Backend Instances
There may be multiple layers of connection pooling and caching.
Changing PooledConnectionLifetime in the application does not necessarily control every connection in the architecture.
Document where DNS resolution occurs and where connections are pooled before using connection lifetime as a failover strategy.
Best Practices
Reuse HttpClient
Use IHttpClientFactory or an appropriately managed long-lived HttpClient.
Configure Connection Lifetime Deliberately
Choose a value based on infrastructure requirements rather than an arbitrary number.
Understand DNS Behavior
Know the DNS TTL and where DNS resolution occurs.
Use Timeouts
Every external dependency should have a reasonable timeout.
Handle Transient Failures
Use bounded retry strategies where appropriate.
Test Actual Failover
Change the destination behind the same hostname and observe real request behavior.
Monitor Failover
Production systems should expose enough telemetry to identify connection failures, backend changes, and recovery times.
Advantages
Helps control how long pooled connections remain active.
Can allow clients to establish fresh connections periodically.
Useful when backend IP addresses can change.
Works naturally with long-lived
HttpClientinstances.Provides more control over connection-pool behavior.
Disadvantages
Short lifetimes can increase connection-establishment overhead.
Connection eviction alone does not guarantee successful failover.
DNS, proxies, load balancers, and other infrastructure can affect the observed behavior.
The correct lifetime depends on the deployment architecture.
Poorly chosen settings can trade one problem for another.
Conclusion
Long-lived HTTP connections are an important performance feature, but they can become complicated when the destination behind a hostname changes.
.NET's HTTP connection management provides ways to control how long pooled connections remain available. By configuring connection lifetime deliberately, an application can periodically establish fresh connections and give DNS changes an opportunity to take effect.
The right approach is not to choose the shortest possible lifetime.
Instead, understand the application's DNS behavior, backend architecture, request frequency, HTTP protocol, and failure-handling requirements. Then test the actual failover scenario by changing the destination behind the same hostname.
Finally, treat connection eviction as one part of a broader reliability strategy. Timeouts, retries, health checks, load balancing, and service discovery still matter.
A controlled DNS failover test can reveal problems that normal functional testing will never expose, making it a useful addition to production-readiness testing for long-lived .NET APIs.
Join the conversation! Your thoughts help the community grow.