Round-robin DNS support in .NET HttpClient

  • Gérald Barré

Round-robin DNS is a load balancing technique where the DNS returns multiple IP address for a host. The client can choose any of the IP address to connect to the server. For example, bing.com has 2 IPv4 addresses:

In .NET, the HttpClient connects to the server using the first IP address in the list. If the connection fails, the client will try the next IP address in the list. If the first IP address succeeds, all clients will use the same IP address, so this reduce the benefits of the round-robin DNS.

You can customize the HttpClient to use a different IP address for each connection. SocketsHttpHandler.ConnectCallback allows to create the connection manually for the HttpClient.

C#
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;

var indexByHosts = new ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var sockerHttpHandler = new SocketsHttpHandler()
{
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
    PooledConnectionLifetime = TimeSpan.FromMinutes(1),
    ConnectCallback = async (context, cancellationToken) =>
    {
        // Get the list of IP addresses for the host
        // ntoe: AddressFamily.Unspecified: IPv4 or IPv6
        var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, AddressFamily.Unspecified, cancellationToken);

        IPAddress[] addresses;
        if (entry.AddressList.Length == 1) // No need to handle round-robin as there is only 1 address
        {
            addresses = entry.AddressList;
        }
        else
        {
            // Compute the first IP address to connect to
            var index = indexByHosts.AddOrUpdate(
                key: entry.HostName,
                addValue: Random.Shared.Next(),
                updateValueFactory: (host, existingValue) => existingValue + 1);

            index %= entry.AddressList.Length;

            if (index == 0)
            {
                // no need to change the addresses
                addresses = entry.AddressList;
            }
            else
            {
                // Rotate the list of addresses
                addresses = new IPAddress[entry.AddressList.Length];
                entry.AddressList.AsSpan(index).CopyTo(addresses);
                entry.AddressList.AsSpan(0, index).CopyTo(addresses.AsSpan(index));
            }
        }

        // Connect to the remote host
        var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
        {
            // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
            NoDelay = true
        };

        try
        {
            await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, cancellationToken);
            return new NetworkStream(socket, ownsSocket: true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
};

var client = new HttpClient(sockerHttpHandler, disposeHandler: true);
C#
await httpClient.GetStringAsync("https://www.bing.com");

#Additional resources

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

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub