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:

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.

MetricWhat It Tells You
DNS resolution resultWhich addresses the resolver sees
Request statusWhether requests succeed
Request latencyWhether failover causes delays
Exception typeWhere failures occur
Connection creationWhether new connections are established
Connection reuseWhether old connections remain active
Recovery timeHow long until normal traffic resumes
Failure countHow 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:

  1. Detects the failed connection.

  2. Creates a new connection.

  3. Resolves the hostname again when appropriate.

  4. Reaches Server B.

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

ComponentResponsibility
DNS APIsResolve hostnames into addresses
SocketsHttpHandlerManage HTTP connections
HttpClientSend HTTP requests
Connection poolReuse established connections
Resilience policiesHandle transient request failures
Service discoveryDetermine 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:

  1. Change DNS to another healthy endpoint.

  2. Remove the DNS record temporarily.

  3. Point DNS to an unavailable endpoint.

  4. Shut down the current server.

  5. Introduce network latency.

  6. Introduce packet loss where the environment supports it.

  7. Restore the original endpoint.

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

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

ScenarioDNSCurrent ServerNew ServerExpected Observation
BaselineStableHealthyHealthyStable traffic
DNS switchChangesHealthyHealthyNew connections eventually use new endpoint
Hard failureStableFailedHealthyClient recovers to available endpoint
Partial failureMultipleSome failedSome healthySuccessful endpoint selection
DNS failureUnavailableExisting connectionN/AExisting connections may continue; new resolution can fail
RecoveryRestoredHealthyHealthyTraffic stabilizes

The important word in the table is eventually.

Failover is not instantaneous simply because DNS changed.

Best Practices

  1. Test DNS resolution and HTTP connectivity separately.

  2. Include connection pooling in failover experiments.

  3. Use controlled DNS changes in a test environment.

  4. Measure recovery time rather than only final success.

  5. Record exception types and request latency.

  6. Test healthy, unhealthy, and partially failed endpoints.

  7. Keep the original hostname when testing HTTPS behavior.

  8. Configure connection lifetime deliberately.

  9. Do not assume DNS TTL predicts exact application failover time.

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