Avoid DNS issues with HttpClient in .NET

 
 
  • Gérald Barré

HttpClient is designed to be instantiated once and reused throughout the lifetime of an application. It maintains a connection pool to minimize the number of open TCP connections. When you send multiple requests to the same host, they share the same connection. This prevents the application from exhausting available sockets under heavy load (You're using HttpClient wrong and it's destabilizing your software), and improves performance by avoiding repeated TCP and TLS handshakes.

Keeping connections open improves performance, but stale connections can cause problems. If a host changes its IP address, existing connections may become invalid once the DNS TTL expires. Those connections should be closed so new ones can be established to the updated address. HttpClient does not handle this automatically because it has no knowledge of DNS TTL values. Instead, you can configure timeouts to close connections automatically. On the next request, a new connection is opened and DNS is queried to resolve the current IP address.

You can use SocketsHttpHandler to configure the behavior of HttpClient and its connection pool. Two properties control this: PooledConnectionIdleTimeout and PooledConnectionLifetime. These properties force HttpClient to close connections after a set amount of time, ensuring the next request to the same host opens a fresh connection and picks up any DNS or network changes.

By default, idle connections are closed after 1 minute, but active connections are never closed. You must explicitly set PooledConnectionLifetime to an appropriate value.

C#
using System.Net;

using var socketHandler = new SocketsHttpHandler()
{
    // The maximum idle time for a connection in the pool. When there is no request in
    // the provided delay, the connection is released.
    // Default value in .NET 6: 1 minute
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),

    // This property defines maximal connection lifetime in the pool regardless
    // of whether the connection is idle or active. The connection is reestablished
    // periodically to reflect the DNS or other network changes.
    // ⚠️ Default value in .NET 6: never
    //    Set a timeout to reflect the DNS or other network changes
    PooledConnectionLifetime = TimeSpan.FromMinutes(1),
};

using var httpClient = new HttpClient(socketHandler);

var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync())
{
    _ = await httpClient.GetStringAsync("https://www.meziantou.net");
}

#Debugging

To observe when HttpClient queries DNS, you can use an EventListener. The System.Net.* objects emit ETW traces that capture this information.

C#
using System.Diagnostics.Tracing;

_ = new NetEventListener();

using var socketHandler = new SocketsHttpHandler()
{
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
    PooledConnectionLifetime = TimeSpan.FromSeconds(10),
};

using var httpClient = new HttpClient(socketHandler);

var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync())
{
    _ = await httpClient.GetStringAsync("https://www.meziantou.net");
}

class NetEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Name.StartsWith("System.Net"))
            EnableEvents(eventSource, EventLevel.Informational);
    }
    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.EventName == "ResolutionStart")
        {
            Console.WriteLine(eventData.EventName + " - " + eventData.Payload[0]);
        }
        else if (eventData.EventName == "RequestStart")
        {
            Console.WriteLine(eventData.EventName + " - " + eventData.Payload[1]);
        }
    }
}

When you run this application, you will see HTTP requests and DNS resolution events logged to the console:

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?