DNS is often invisible when an application is working correctly. A .NET application requests api.example.com, the operating system resolves the hostname, and HttpClient connects to the resulting endpoint.
The problem appears when infrastructure changes.
A service may move to another IP address, a failed instance may be replaced, or a deployment may change the set of available endpoints. At that point, an application must correctly handle DNS resolution, caching, connection reuse, and failure recovery.
Modern .NET networking APIs provide more control over DNS-related operations, but simply resolving a hostname successfully does not prove that an application can survive a real DNS failover.
The more useful engineering question is:
What happens to a long-running .NET application when the endpoint behind a hostname changes while traffic is still flowing?
This article builds a practical test strategy for that scenario and shows how to distinguish DNS resolution problems from HTTP connection-pool problems.
Why DNS Failover Testing Matters
Consider a service running behind a hostname:
api.example.internal
|
v
DNS Resolver
|
+----+----+
| |
v v
Server A Server B
10.0.0.10 10.0.0.20
Initially, the hostname resolves to Server A.
During a failure or deployment, the DNS record changes:
api.example.internal
|
v
DNS Resolver
|
v
Server B
10.0.0.20
A newly created connection may discover Server B.
An existing TCP connection to Server A does not suddenly change its destination because DNS changed.
This distinction is fundamental.
DNS resolution
!=
Existing TCP connection
A realistic failover test therefore needs to evaluate both.
DNS Resolution in a .NET Application
At the simplest level, an application can resolve a hostname using System.Net.Dns.
For example:
using System.Net;
var addresses = await Dns.GetHostAddressesAsync(
"api.example.internal");
foreach (var address in addresses)
{
Console.WriteLine(address);
}
This is useful when an application explicitly needs address information.
However, resolving a hostname manually does not automatically change where HttpClient sends requests.
For example:
var addresses = await Dns.GetHostAddressesAsync(
"api.example.internal");
using var client = new HttpClient();
var response = await client.GetAsync(
"https://api.example.internal/health");
The manual DNS lookup and the HTTP request are separate operations.
The HTTP stack still performs its own endpoint handling.
Why Manual DNS Resolution Can Be Misleading
A common testing mistake is:
1. Resolve hostname.
2. Verify new IP address.
3. Assume HttpClient is now using the new IP.
That conclusion is not necessarily valid.
The application may still have:
An existing pooled connection
A DNS cache
A proxy
A load balancer
Another networking layer between the client and service
Therefore, DNS testing must distinguish name resolution from connection selection.
Build a Simple DNS Diagnostic Tool
A small diagnostic utility can record DNS results over time:
using System.Net;
const string host = "api.example.internal";
while (true)
{
try
{
var addresses =
await Dns.GetHostAddressesAsync(host);
Console.WriteLine(
$"{DateTimeOffset.UtcNow:O}");
foreach (var address in addresses)
{
Console.WriteLine($" {address}");
}
}
catch (SocketException ex)
{
Console.WriteLine(
$"DNS failure: {ex.SocketErrorCode}");
}
await Task.Delay(TimeSpan.FromSeconds(5));
}
This gives you a simple timeline:
09:00:00 → 10.0.0.10
09:00:05 → 10.0.0.10
09:00:10 → 10.0.0.20
That proves the resolver eventually observed the DNS change.
It does not yet prove that your HTTP client switched to the new endpoint.
Test DNS Failover With a Controlled Environment
A useful test environment contains two HTTP servers.
api.test
|
DNS
|
+------+------+
| |
v v
Server A Server B
Port 5001 Port 5002
Start with:
api.test → Server A
Then change the DNS mapping:
api.test → Server B
The test client continuously sends requests:
Client
|
+-- Request 1 → A
+-- Request 2 → A
+-- Request 3 → A
|
DNS changes
|
+-- Request 4 → ?
+-- Request 5 → ?
+-- Request 6 → B
The question mark is exactly what the experiment should measure.
Connection Pooling Changes the Result
HttpClient uses connection pooling.
Suppose the client establishes:
api.test
|
+--> 10.0.0.10:443
The next request may reuse that connection.
If DNS changes:
api.test
|
+--> 10.0.0.20:443
the existing connection to 10.0.0.10 remains an existing connection.
This is why DNS failover tests should control connection lifetime.
A test client can use:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime =
TimeSpan.FromSeconds(15)
};
using var client = new HttpClient(handler)
{
BaseAddress = new Uri(
"https://api.test")
};
The lifetime should be deliberately short in a test environment so connection replacement occurs quickly.
For production, the value should be selected based on the infrastructure's actual requirements.
Build a Continuous Failover Test
A basic test client might look like this:
using System.Diagnostics;
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime =
TimeSpan.FromSeconds(15)
};
using var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.test")
};
while (true)
{
var stopwatch = Stopwatch.StartNew();
try
{
using var response =
await client.GetAsync("/health");
stopwatch.Stop();
Console.WriteLine(
$"{DateTimeOffset.UtcNow:O} " +
$"Status={(int)response.StatusCode} " +
$"Latency={stopwatch.ElapsedMilliseconds}ms");
}
catch (Exception ex)
{
stopwatch.Stop();
Console.WriteLine(
$"{DateTimeOffset.UtcNow:O} " +
$"Failure={ex.GetType().Name} " +
$"Latency={stopwatch.ElapsedMilliseconds}ms");
}
await Task.Delay(1000);
}
This test records an important sequence:
Normal traffic
↓
DNS change
↓
Connection transition
↓
Possible failures
↓
Traffic reaches new endpoint
The test should not be judged only on whether the final request succeeds.
The transition period is the interesting part.
Measure More Than DNS Resolution
A useful failover test should collect several metrics.
| Metric | What It Tells You |
|---|---|
| DNS resolution result | Which addresses the resolver sees |
| Request status | Whether requests succeed |
| Request latency | Whether failover causes delays |
| Exception type | Where failures occur |
| Connection creation | Whether new connections are established |
| Connection reuse | Whether old connections remain active |
| Recovery time | How long until normal traffic resumes |
| Failure count | How disruptive the transition was |
A useful definition of recovery time is:
Recovery Time =
Time of first stable successful request
-
Time failover started
The exact definition should be documented before running the experiment.
Test Hard Failover
A DNS record changing cleanly is only one scenario.
Test what happens when Server A becomes unavailable.
Before:
Client → Server A
Server B available
Failure:
Client → Server A X
Server B available
Now observe whether the client:
Detects the failed connection.
Creates a new connection.
Resolves the hostname again when appropriate.
Reaches Server B.
Recovers without excessive retries.
This exposes problems that a simple DNS-resolution test cannot find.
Test Partial Failover
Real systems may have multiple DNS records.
For example:
api.example.com
10.0.0.10 Server A
10.0.0.20 Server B
10.0.0.30 Server C
A useful test should not assume that every returned address is healthy.
Test scenarios such as:
A = Healthy
B = Healthy
C = Healthy
Then:
A = Failed
B = Healthy
C = Healthy
And:
A = Failed
B = Failed
C = Healthy
This reveals how the application behaves when only part of the endpoint set is available.
DNS APIs and HttpClient Solve Different Problems
A useful architectural distinction is:
| Component | Responsibility |
|---|---|
| DNS APIs | Resolve hostnames into addresses |
| SocketsHttpHandler | Manage HTTP connections |
| HttpClient | Send HTTP requests |
| Connection pool | Reuse established connections |
| Resilience policies | Handle transient request failures |
| Service discovery | Determine available service endpoints |
Avoid putting all failover responsibility into a single layer.
For example, manually resolving DNS and selecting an IP address for every HTTP request can bypass important HTTP connection-management behavior and may introduce additional complexity.
Avoid Hard-Coding Resolved IP Addresses
This pattern is usually problematic:
var addresses =
await Dns.GetHostAddressesAsync("api.example.com");
var client = new HttpClient
{
BaseAddress =
new Uri($"https://{addresses[0]}")
};
HTTPS introduces another problem: the hostname is normally important for TLS certificate validation and server-name indication.
Using the resolved IP address directly can therefore change TLS behavior and certificate validation.
If the objective is simply to test DNS failover, keep the original hostname in the HTTP request and test the networking stack as it would operate in production.
DNS Failure Is Not the Same as HTTP Failure
Consider three different situations.
DNS Failure
Hostname
↓
DNS
X
No address
The application cannot determine where to connect.
Connection Failure
Hostname
↓
IP Address
↓
TCP connection
X
DNS worked, but the network connection failed.
HTTP Failure
Hostname
↓
IP Address
↓
Connection
↓
HTTP request
↓
500 / 503 / timeout
The network connection succeeded, but the service failed at the application layer.
These failures should be recorded separately.
Testing With Fault Injection
For realistic resilience testing, introduce failures deliberately.
Useful scenarios include:
Change DNS to another healthy endpoint.
Remove the DNS record temporarily.
Point DNS to an unavailable endpoint.
Shut down the current server.
Introduce network latency.
Introduce packet loss where the environment supports it.
Restore the original endpoint.
Observe recovery.
A production-style test might therefore look like:
Baseline
↓
DNS change
↓
Observe
↓
Endpoint failure
↓
Observe
↓
Restore service
↓
Observe recovery
The objective is to understand the system's behavior rather than simply prove that a DNS API returns an address.
Common Mistakes
Testing DNS Only Once
One successful lookup says nothing about failover behavior.
Run resolution repeatedly while changing the underlying environment.
Ignoring Connection Reuse
A DNS change does not automatically terminate existing TCP connections.
Always include connection-pool behavior in the test design.
Using Extremely Short Timeouts
If the timeout is too aggressive, the test may report failures caused by the test configuration rather than the DNS transition.
Testing Only Healthy Failover
A healthy DNS switch is the easy case.
Also test unreachable and partially failed endpoints.
Treating DNS TTL as a Guaranteed Application Behavior
TTL influences caching, but the complete behavior depends on the DNS infrastructure and resolver path.
Do not use TTL alone to predict exactly when a .NET application will connect to a new endpoint.
Troubleshooting
DNS Shows the New IP but HTTP Requests Still Reach the Old Server
Check connection pooling first.
An existing pooled connection can continue to serve requests even after DNS resolution changes.
DNS Resolution Fails but Existing HTTP Requests Continue Working
This can be expected.
An already-established connection does not necessarily require a fresh DNS lookup for every request.
Failover Causes a Large Latency Spike
Measure:
DNS lookup latency
TCP connection establishment
TLS negotiation
Connection-pool availability
Server response time
The latency increase may come from creating a new connection rather than DNS itself.
Requests Fail During Every Endpoint Change
Check whether the application has an appropriate connection lifetime and resilience strategy.
Connection eviction and retry behavior should be tested together rather than independently.
Recommended Failover Test Matrix
| Scenario | DNS | Current Server | New Server | Expected Observation |
|---|---|---|---|---|
| Baseline | Stable | Healthy | Healthy | Stable traffic |
| DNS switch | Changes | Healthy | Healthy | New connections eventually use new endpoint |
| Hard failure | Stable | Failed | Healthy | Client recovers to available endpoint |
| Partial failure | Multiple | Some failed | Some healthy | Successful endpoint selection |
| DNS failure | Unavailable | Existing connection | N/A | Existing connections may continue; new resolution can fail |
| Recovery | Restored | Healthy | Healthy | Traffic stabilizes |
The important word in the table is eventually.
Failover is not instantaneous simply because DNS changed.
Best Practices
Test DNS resolution and HTTP connectivity separately.
Include connection pooling in failover experiments.
Use controlled DNS changes in a test environment.
Measure recovery time rather than only final success.
Record exception types and request latency.
Test healthy, unhealthy, and partially failed endpoints.
Keep the original hostname when testing HTTPS behavior.
Configure connection lifetime deliberately.
Do not assume DNS TTL predicts exact application failover time.
Validate behavior under realistic production network conditions.
Frequently Asked Questions
Does changing DNS immediately move existing HttpClient connections?
No. Existing connections are independent of subsequent DNS resolution.
Should I resolve DNS manually before every HTTP request?
Usually not. HttpClient and its underlying handler already manage endpoint resolution and connections. Manual resolution should have a specific architectural purpose.
Can DNS failover eliminate HTTP retries?
No. DNS failover and request resilience solve different problems. A connection can fail after DNS succeeds, and an HTTP request can fail after a connection is established.
How can I verify that failover actually happened?
Use endpoint-specific diagnostics, server-side logs, request tracing, and connection telemetry. DNS output alone is insufficient.
What should I benchmark during DNS failover?
At minimum, measure DNS resolution time, connection establishment, request latency, failure count, and recovery time.
Conclusion
DNS failover is easy to demonstrate with a simple hostname lookup, but real application failover is considerably more complex.
A production .NET application sits between several layers: DNS resolution, connection pooling, TCP, TLS, HTTP, proxies, load balancers, and application-level resilience. A DNS record changing is only one event in that chain.
The most useful test therefore follows the entire transition from the old endpoint to the new one.
By combining controlled DNS changes, long-running HttpClient instances, connection-lifetime configuration, endpoint failures, and detailed telemetry, developers can determine whether an application merely resolves DNS correctly or actually recovers correctly when infrastructure changes underneath it.
That distinction is what makes DNS testing valuable for production .NET systems.

Jasen FiciPosted Aug 20, 2026, 1:47 PM
This was included in DotNetNews here: https://dotnetnews.co/archive/the-net-news-daily-issue-523/