Last Updated on March 5, 2022


The HttpClient class, introduced since .NET Framework 4.5, is probably one of the most used classes in the .NET platform. It exposes methods allowing developers to access resources on the internet by sending HTTP requests to servers hosting these resources.

Before .NET Framework 4.5, the HttpWebRequest class was mostly used to achieve the same purpose. This may explain why the first version of HttpClient (in .NET Framework 4.5) was built on top of HttpWebRequest, before evolving into a new implementation not based on HttpWebRequest in .NET Core.  

I have been using the HttpClient class for many years now, and there are some interesting differences to pay attention to if your project is targeting .NET Framework or .NET Core. In this article, we will focus on the differences in the connection management layer.

HttpClient overview

Before moving forward, let’s take a quick look at the HttpClient class itself. The following code shows an example where we are sending an HTTPS request to retrieve a resource on the internet:

static readonly HttpClient MyHttpClient = new HttpClient();

static async Task Main(string[] args)
{
    try
    {
        string requestUrl = "https://www.google.com";
        Console.WriteLine($"GET {requestUrl}");
        HttpResponseMessage response = await MyHttpClient.GetAsync(requestUrl);
        response.EnsureSuccessStatusCode();

        string responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"------------Response Body------------");
        Console.WriteLine(responseBody);
        Console.WriteLine($"------------End of Response Body------------");
    }
    catch (HttpRequestException e)
    {
        Console.WriteLine($"HttpRequestException: {e}");
    }
}

In the documentation of the HttpClient class, Microsoft recommends instantiating only 1 instance of this class and re-using that instance throughout the lifecycle of your application. This explains why we are sharing a static HttpClient instance across the whole application.

Even though the HttpClient class implements the IDisposable interface, you should not instantiate and dispose multiple instances of HttpClient in your application. The reason behind this recommendation is explained here.

In a nutshell, when you dispose an HttpClient object, the TCP connections opened by that instance will transition to TIME_WAIT state and will not be closed immediately. So, creating and disposing many HttpClient instances can lead to a socket exhaustion issue on the machine.

Therefore, you need to make sure that there is only 1 instance of HttpClient in your application. You can achieve that by either using :

However, if you are coding a service or a long-running process, using only 1 instance of HttpClient can create another issue related to DNS changes. In such a case, when there are DNS changes, the long-running process will not be aware of the changes since it will not restart the DNS resolution again for active connections. We will talk about different ways of addressing this issue later.

HttpClient connections management in .NET Framework 4.5 +

In .NET Framework 4.5 and +, the default constructor of HttpClient uses the HttpClientHandler class, which leverages the HttpWebRequest class to send HTTP requests.

HttpClient in .NET Framework 4.5 and +

Therefore, we can use the good old ServicePoint and ServicePointManager classes to manage opened connections. Basically, a service point is identified by the host and the scheme sections of the URI. For example, all URI starting with https://www.google.com will use the same service point.

Here is how to set the maximum number of concurrent connections per service point object:

//Set maximum number of connections per service point to 5
ServicePointManager.DefaultConnectionLimit = 5;

The default connection limit is 10 for ASP.NET hosted applications and 2 for other types of processes.

In addition, you can change the connection limit on already existing service points with the following code:

//Set maximum number of connections to 5 for a specific service point
Uri requestUri = new Uri("https://www.google.com");
ServicePoint servicePoint = ServicePointManager.FindServicePoint(requestUri);
if(servicePoint != null)
{
    servicePoint.ConnectionLimit = 5;
}

NB: This code example is exactly what happens in the background when you set the MaxConnectionsPerServer property of the HttpClientHandler in .NET Framework 4.5 and +. At the time of writing this article, the latest version of .NET Framework is 4.8. Therefore, the term NET 4.5 and +, refers to .NET Framework versions from 4.5 to 4.8.

Testing the concept

In the following example, we will set the connection limit to 4 and send 20 simultaneous requests to the same endpoint. During that test, we will keep the TcpView tool opened to verify the number of connections created for that specific service point.

static readonly HttpClient MyHttpClient = new HttpClient();

static async Task Main(string[] args)
{
    ServicePointManager.DefaultConnectionLimit = 4;
    string requestUrl = "https://www.google.com";

    IEnumerable<Task> tasks = Enumerable.Range(1, 20).Select(i => Task.Run(async () =>
        {
            try
            {
                Console.WriteLine($"GET {requestUrl}. Iteration: {i}");

                HttpResponseMessage response = await MyHttpClient.GetAsync(requestUrl);
                response.EnsureSuccessStatusCode();

                string responseBody = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"ResponseBody Length For Iteration {i} is: {responseBody.Length}");

            }
            catch (HttpRequestException e)
            {
                Console.WriteLine($"HttpRequestException For Iteration {i}: {e.Message}");
            }
        }));

     await Task.WhenAll(tasks);

     Console.WriteLine($"Press Enter to exit....");
     Console.ReadLine();
}

Here is the result of the test:

HttpClient connections tests result from TcpView while using .NET Framework

As you can see, the test tool only opened 4 TCP connections to the specified destination, even though we sent 20 HTTP requests.




HttpClient connections management in .NET Core 1.0, 1.1 and 2.0

In these versions of .Net Core, the HttpClient class uses the Windows native WinHttpHandler class, which does not rely at all on HttpWebRequest. Moreover, for platforms like Linux and macOS, HttpClient uses the CurlHandler class, which is an internal class that leverages libcurl-based HTTP transport on these platforms.

Basically, in these versions of .NET Core, the ServicePointManager class does not have any impact on the number of connections opened. In .NET Core 2.0 for example, your code may even throw a PlatformNotSupportedException if you try to use some of the methods of ServicePoint and ServicePointManager.   

At the time of writing this article, .NET Core 1.0, 1.1, and 2.0 are not supported anymore by Microsoft. Therefore, if you are still using one of these versions, I highly recommend upgrading at least to .NET Core 3.1. You can find all information about the .NET Core support policy here.

NB: If you cannot upgrade to a newer version of .NET Core, then you can try to use the MaxConnectionsPerServer property on the HttpClientHandler instance itself.

HttpClient connections management in .NET Core 2.1 +

With .NET Core 2.1, Microsoft did some interesting changes in HttpClient implementation. The HttpClient class now uses by default the SocketsHttpHandler class, which leverages the sockets API directly. This provides a good performance improvement compared to the previous versions of HttpClient and consistency across all platforms. For example, on macOS and Linux, there is no dependency on libcurl anymore.

HttpClient in .NET Core 2.1 and +

In addition, the SocketsHttpHandler class has a connection pooling mechanism to manage opened connections per unique endpoint. Basically, after sending an HTTP request, the SocketsHttpHandler class will add the TCP connection to the pool in order to reuse it in the future for subsequent requests to the same endpoint.

Here are the properties of the SocketsHttpHandler that you need to be aware of to manage the connection pool:

  • MaxConnectionPerServer: The maximum number of TCP connections allowed per single endpoint. If you output this value in the debugger for a new instance of SocketsHttpHandler, you will notice that the default value is equal to Int32.MaxValue.
  • PooledConnectionIdleTimeout: Determines how long a TCP connection can stay unused in the pool. After this timeout, the SocketsHttpHandler object will remove the connection from the pool.
  • PooledConnectionLifetime: The amount of time an active TCP connection can remain in the pool. The word “active” refers to a connection that is currently being used (i.e.. not in idle mode).

Now, let’s play with these settings and observe the behavior of the connection pooling mechanism.

NB: Here we are referring to .NET Core 2.1, 2.2, 3.0, 3.1 and .NET 5.0. Note also that .NET core 2.1, 2.2, and 3.0 are also no longer supported by Microsoft as well.

Testing the maximum connection per server

Let’s configure the MaximumConnectionPerServer to 3 and do the exact same test that we did previously.

static async Task Main(string[] args)
{
    var socketsHandler = new SocketsHttpHandler
    {
        MaxConnectionsPerServer = 3
    };

    HttpClient myHttpClient = new HttpClient(socketsHandler);
    string requestUrl = "https://www.google.com";

    IEnumerable<Task> tasks = Enumerable.Range(1, 20).Select(i => Task.Run(async () =>
        {
           try
           {
               Console.WriteLine($"GET {requestUrl}. Iteration: {i}");

               HttpResponseMessage response = await myHttpClient.GetAsync(requestUrl);
               response.EnsureSuccessStatusCode();

               string responseBody = await response.Content.ReadAsStringAsync();
               Console.WriteLine($"ResponseBody Length For Iteration {i} is: {responseBody.Length}");

            }
            catch (HttpRequestException e)
            {
               Console.WriteLine($"HttpRequestException For Iteration {i}: {e.Message}");
            }
        }));

     await Task.WhenAll(tasks);

     Console.WriteLine($"Press Enter to exit....");
     Console.ReadLine();
}

Here is the result for this test:

Tests result from TcpView

As you can see, the test tool opened only 3 TCP connections to the specified server.

Testing the pooled connection idle time

In the following example, we will set the maximum connection to 1 and the pool idle time to 2 seconds and then wait until the expiration of the idle time before sending another request. Here is the code sample.

static async Task Main(string[] args)
{
    var socketsHandler = new SocketsHttpHandler
    {
        MaxConnectionsPerServer = 1,
        PooledConnectionIdleTimeout = TimeSpan.FromSeconds(2),
    };

    HttpClient myHttpClient = new HttpClient(socketsHandler);
    string requestUrl = "https://www.google.com";

    for (int i = 1; i <= 10; i++)
    {
        try
        {
            Console.WriteLine($"GET {requestUrl}. Iteration: {i}");

            HttpResponseMessage response = await myHttpClient.GetAsync(requestUrl);
            response.EnsureSuccessStatusCode();

            string responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"ResponseBody Length For Iteration {i} is: {responseBody.Length}");

            await Task.Delay(TimeSpan.FromSeconds(5));
        }
        catch (HttpRequestException e)
        {
            Console.WriteLine($"HttpRequestException For Iteration {i}: {e.Message}");
        }
    }

    Console.WriteLine($"Press Enter to exit....");
    Console.ReadLine();
}

Here is the result of the test. This time, the DNS returned IP address 172.217.13.164 for the domain name google.com.

Second Tests result from TcpView

In this screenshot, you can see that there are 10 TCP connections in the TIME-WAIT state. Since we configured the PooledConnectionIdleTimeout to 2 seconds, if the connection is idle for more than 2 seconds then it will move from Established state to TIME-WAIT state. Consequently, the HttpClient object will open a new TCP connection for the next iteration of the test, which happens 5 seconds later.

Another alternative to manage HttpClient connections in .NET Core 2.1+: IHTTPClientFactory.

Microsoft introduced the IHttpClientFactory interface with .NET core 2.1 in order to address the sockets exhaustion and the DNS changes issues that we mentioned previously.

With IHttpClientFactory, you can manage the creation of your HttpClient object through Dependency Injection and take advantage of the typed and named clients as well.

NB: In this section, I will not go over the details of the named or typed client, since it’s not the focus of this article.

From the connection management perspective, the IHttpClientFactory interface gives the convenience of keeping a pool of HttpMessageHandler objects and manage the lifetime of these objects. Therefore, when you get a new HttpClient from the DI, this client will be instantiated with an HttpMessageHandler coming from the pool. And when you are done with the HttpClient object, the HttpMessageHandler object is returned to the pool.

Here is a simple way to register the HttpClientFactory:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("azureManagementApi", c =>
    {
        c.BaseAddress = new Uri("https://management.azure.com");
        c.DefaultRequestHeaders.Add("Accept", "application/json");
    });
}

And then once the IHttpClientFactory interface is injected from the DI in another class, you can create a new HttpClient by calling this method:

//Here, clientFactory is an object of type IHttpClientFactory
HttpClient client = clientFactory.CreateClient("azureManagementApi");

Moreover, you can also configure the lifetime of the HttpMessageHandler in the pool by calling the SetHandlerLifetime method during the registration in the DI. That’s also why IHttpClientFactory is very useful when you have a long-running process because it will recycle the HttpMessageHandler regularly throughout the lifecycle of your process.

Handling DNS Changes when using a singleton HttpClient

As mentioned previously, if you are using a single HttpClient instance in a long-running process, your process will not be able to catch DNS changes.  Basically, the HttpClient object does a DNS resolution before opening the TCP connection. Once the TCP connection is opened, the DNS resolution will not be done again until the HTTP Handler or the ServicePointManager decides to open another TCP connection to that specific server.

Therefore, to be able to follow DNS changes, you need at some point to close and open existing TCP connections that are in the pool. That’s where the connection lifetime comes into play. The value provided for the connection lifetime cannot be infinite if we want to be able to handle DNS changes correctly with only 1 instance of HttpClient.

NB: This DNS issue happens only if the long-running process is continuously using the active TCP connection. It does not affect idle connections, because Idle connections will be closed automatically based on the idle connection timeout, which is completely different from the active connection lifetime.

If you are using .NET Framework 4.5 and +, you can use the service point property to manage the connection lease timeout. The default value is -1 which means that an active connection remains open forever.

//Set Connection Lease Timeout to 1 hour for example
Uri requestUri = new Uri("https://www.google.com");
ServicePoint servicePoint = ServicePointManager.FindServicePoint(requestUri);
if (servicePoint != null)
{
    servicePoint.ConnectionLeaseTimeout = 3600 * 1000;
}

If you are using .Net Core 2.1 and +, you can either:

  • Set the PooledConnectionLifetime of the SocketsHttpHandler object
  • Or use the IHttpClientFactory and set the handler lifetime.
//Set the PooledConnectionLifetime 
var socketsHandler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(30)
};

HttpClient myHttpclient = new HttpClient(socketsHandler);
//Or use the IHttpClientFactory and set the handler lifetime
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("azureManagementApi", c =>
    {
        c.BaseAddress = new Uri("https://management.azure.com");
        c.DefaultRequestHeaders.Add("Accept", "application/json");
    }).SetHandlerLifetime(TimeSpan.FromMinutes(30));
}

Summary

In a nutshell, here is how you can manage the connections when using the HttpClient in .NET:

HttpClient connections management in .NET