Modern distributed applications rarely communicate with a single fixed server.
A production API may sit behind a load balancer, service discovery layer, DNS-based failover system, or cloud infrastructure that changes endpoint addresses over time.
That creates a subtle networking problem for long-running .NET applications.
A service can continue using an established HTTP connection even after DNS has changed.
Consider this simplified architecture:
Application
↓
api.example.internal
↓
10.10.10.20
Later, the infrastructure changes:
api.example.internal
↓
10.10.10.35
New DNS resolution may return the new address, but an existing connection can remain associated with the previous endpoint.
For short-lived processes this may not matter.
For long-running APIs, workers, and background services, stale connections can become a real availability problem.
The solution is not to resolve DNS manually before every request. A better approach is to build an HTTP client strategy that understands DNS changes, connection lifetime, failure detection, and controlled endpoint rotation.
Why DNS Changes Are Difficult for Long-Running Clients
DNS is commonly treated as:
Name
↓
IP address
But an HTTP client has more state than that.
A simplified connection lifecycle looks like:
DNS lookup
↓
TCP connection
↓
TLS handshake
↓
HTTP requests
↓
Connection reuse
If the DNS record changes after the connection is established, the client may continue using the existing connection.
That means:
DNS
10.10.10.20 → 10.10.10.35
does not automatically imply:
Existing TCP connection
10.10.10.20 → 10.10.10.35
The connection has its own lifecycle.
Why HttpClient Lifetime Matters
A common .NET recommendation is to avoid creating a new HttpClient for every request.
Instead, applications typically reuse clients through IHttpClientFactory or another managed lifetime strategy.
For example:
builder.Services.AddHttpClient<OrdersClient>();
This provides connection reuse and centralized configuration.
But connection reuse creates another question:
How long should an established connection remain valid?
If connections remain alive indefinitely, DNS changes may take longer to influence actual network traffic.
Configure Connection Lifetime
SocketsHttpHandler provides connection lifetime controls.
For example:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
};
var client = new HttpClient(handler);
The idea is straightforward:
Connection created
↓
Used for requests
↓
Lifetime expires
↓
Connection removed from pool
↓
New connection created
↓
DNS resolution can occur again
This provides a controlled way to allow DNS changes to eventually influence new connections.
Why Not Use a Very Short Lifetime?
You might be tempted to configure:
PooledConnectionLifetime = TimeSpan.FromSeconds(1);
That can undermine connection pooling.
Every new connection can require:
DNS
↓
TCP
↓
TLS
For HTTPS services, repeated TLS handshakes can add CPU consumption and latency.
The objective is therefore not:
Refresh DNS as fast as possible
but:
Balance connection reuse with endpoint freshness
DNS-Aware Failover Architecture
A resilient client can be designed around several layers:
HTTP Client
|
Connection Pool
|
+-----------+-----------+
| |
Connection Age Request Failure
| |
v v
Rotate connection Retry policy
| |
+-----------+-----------+
|
New connection
|
DNS resolution
The connection pool handles normal endpoint rotation.
The retry layer handles transient failures.
These are different responsibilities.
Do Not Treat Retries as DNS Refresh
Suppose a request fails because an endpoint has disappeared.
A naive retry might simply send the request again over another existing connection.
That does not necessarily solve the underlying endpoint problem.
A better strategy is:
Request
↓
Failure
↓
Determine failure type
↓
Allow connection rotation
↓
Retry when safe
Retries should not become an excuse to ignore connection lifecycle management.
Configure HttpClientFactory
For ASP.NET Core applications, a named or typed client is usually easier to manage.
For example:
builder.Services.AddHttpClient<PaymentsClient>()
.ConfigurePrimaryHttpMessageHandler(() =>
new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
});
The exact values should be based on the DNS TTL, infrastructure behavior, traffic pattern, and acceptable connection churn.
There is no universal five-minute setting that is correct for every system.
DNS TTL and Connection Lifetime Are Different
DNS TTL tells resolvers how long a DNS answer can be cached.
Connection lifetime controls how long an established HTTP connection can remain in the pool.
They are related, but they are not the same thing.
For example:
DNS TTL: 60 seconds
Connection lifetime: 10 minutes
The application may continue using an existing connection beyond the DNS record's TTL.
Conversely:
DNS TTL: 10 minutes
Connection lifetime: 30 seconds
the application may establish connections more frequently than necessary.
The configuration should therefore consider both infrastructure behavior and application traffic.
Use Failure Signals as Part of Failover
Connection lifetime alone does not make a client highly available.
Consider:
Primary endpoint
↓
Connection established
↓
Endpoint becomes unhealthy
Waiting for a five-minute connection lifetime may be too slow.
Applications should therefore detect meaningful failures and respond appropriately.
Useful signals include:
Connection reset
Connection refused
Timeout
DNS resolution failure
HTTP 5xx responses
Service-specific health failures
However, not every HTTP error should trigger a retry.
A 400 Bad Request is normally an application error, not a transient infrastructure failure.
Retry Only Safe Operations
Retries can create duplicate operations.
For example:
POST /payments
might successfully reach the server even though the client does not receive the response.
If the client blindly retries:
POST /payments
POST /payments
the operation could be performed twice.
For idempotent operations such as many GET requests, retrying transient failures is generally easier to reason about.
For non-idempotent operations, use application-level idempotency mechanisms where appropriate.
Join the conversation! Your thoughts help the community grow.