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.
Join the conversation! Your thoughts help the community grow.