Round-robin DNS support in .NET HttpClient

 
 
  • Gérald Barré

Round-robin DNS is a load-balancing technique in which a DNS server returns multiple IP addresses for a single host, allowing clients to connect to any of them. For example, bing.com resolves to 2 IPv4 addresses:

By default, HttpClient in .NET connects using the first IP address returned by DNS. It only falls back to the next address if the connection fails. When the first address succeeds, every subsequent connection reuses it, which negates the load-balancing benefit of round-robin DNS.

You can customize HttpClient to distribute connections across all available IP addresses. SocketsHttpHandler.ConnectCallback lets you create the TCP connection manually, giving you full control over which address is used.

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
        // note: 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 httpClient = 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?