Avoid DNS issues with HttpClient in .NET

  • Gérald Barré

HttpClient allows to send HTTP requests. HttpClient is intended to be instantiated once and re-used throughout the life of an application. Indeed, the HttpClient has a connection pool to reuse connections and reduce the number of TCP connections. So, if you send multiple requests to the same host, they will reuse the same connection. This way the application won't exhaust the number of sockets available under heavy loads (You're using HttpClient wrong and it's destabilizing your software). Also, this improves the performance of the application by avoiding handshakes (TCP handshake, TLS handshake) for each request to the same host.

Keeping connections opened is a good thing in terms of performance, but you have to be careful to not keep stale connections. What if the host has changed its IP address? For instance, if the DNS TTL is expired, the host may change its IP address. In this case, the opened connections should be closed and a new connection should be opened. The HttpClient doesn't do this automatically as it has no knowledge about the DNS TTL. Instead, you can provide timeouts to automatically close the connection. This way, the next request will need to reopen a connection and the DNS will be used to find the new IP address.

You can use the SocketsHttpHandler to configure the behavior of the HttpClient and its connection pool. There are 2 properties to configure: PooledConnectionIdleTimeout and PooledConnectionLifetime. These properties allow to force the HttpClient to close the connection after a certain amount of time. This way, the next request to the same host will need to open a new connection and so, to reflect the DNS or other network changes.

By default, idle connections are closed after 1 minute. However, active connections are never closed. You have to explicitly set PooledConnectionLifetime to the desired 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

If you want to know when the HttpClient instance is querying the DNS, you can use an EventListener. Indeed, the System.Net.* objects emit ETW traces.

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 should see when the application is doing http requests and DNS requests:

#Additional resources

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

Follow me:
Enjoy this blog?Buy Me A Coffee