Why you should reuse HttpClient instances?

I come across this every so often. It then usually involves me Googling the same stack overflow answers to explanation why one shouldn't do it.

using (var client = new HttpClient())  
{
    string result = await client.GetStringAsync(requestUri);

    // ... do something with the result ...
}

What's wrong with the above code? It's disposing the HttpClient with every request, which is generally not recommended.

TL;DR

Having conducted a simple test, my example found that HTTPS requests were 4× faster when reusing HttpClient instances. Below is the summary:

  HTTP Request HTTPS Request
Disposed Client 72ms/request 154ms/request
Reused Client 38ms/request 36ms/request

Why is it wrong?

Even though it's counter intuitive, given HttpClient implements IDisposable, it should not be disposed with every request, but reused, often maintained for the lifetime of the application. HttpClient is thread safe and can be maintained as a static variable, accessed from any thread.

The stack overflow answers I usually find are these:

These answers confirm HttpClient instances should be kept around, but I was wondering, what is the impact on performance? So I decided to try it out myself.

Testing whether I should dispose HttpClient

I want to see for myself why I should not dispose HttpClient instances, but reuse them for multiple requests. So I created a simple console application.

Firstly, two methods to make requests, one reusing an HttpClient, the other disposing an instance every request:

HttpClient _singletonClient;

async Task UseSingletonClientAsync(Uri requestUri)  
{
    await _singletonClient.GetStringAsync(requestUri);
}

async Task UseDisposingClientAsync(Uri requestUri)  
{
    using (var client = new HttpClient())
    {
        await client.GetStringAsync(requestUri);
    }
}

Then a method taking either of the methods above and calling each a specified number of times.

async Task TimeAsync(Uri requestUri, Func<Uri, Task> funcAsync, int count)  
{
    var sw = new Stopwatch();
    sw.Start();

    for (int i = 0; i < count; ++i)
    {
        await funcAsync(requestUri);
    }

    sw.Stop();

    Console.WriteLine($"Request '{requestUri}' {count} times taking '{sw.Elapsed}'. " +
                      $"Average {sw.ElapsedMilliseconds/count}ms/request.");
}

Lastly, calling these from the Main method of the console app.

static void Main(string[] args)  
{
    _singletonClient = new HttpClient();

    var httpUri = new Uri("http://gimmeip.azurewebsites.net/");
    var httpsUri = new Uri("https://gimmeip.azurewebsites.net/");

    // ... warm the services, omitted for brevity.

    Console.WriteLine("Using Disposing Client.");
    TimeAsync(httpUri,  UseDisposingClientAsync, 100).Wait();
    TimeAsync(httpsUri, UseDisposingClientAsync, 100).Wait();

    Console.WriteLine("Using Singleton Client.");
    TimeAsync(httpUri,  UseSingletonClientAsync, 100).Wait();
    TimeAsync(httpsUri, UseSingletonClientAsync, 100).Wait();
}

I'm making a request to a very simple node app that returns the client's IP address. There are many equivalent sites available that do the same, but I'm potentially DOS attacking the site, so I decided use my own. The code for the site is in my GitHub repository gimmeip.

Result

Here's the output of the console app:

Using Disposing Client.  
Request 'http://gimmeip2.azurewebsites.net/' 100 times taking '07.298'. Average 72ms/request.  
Request 'https://gimmeip2.azurewebsites.net/' 100 times taking '15.440'. Average 154ms/request.

Using Singleton Client.  
Request 'http://gimmeip2.azurewebsites.net/' 100 times taking '03.864'. Average 38ms/request.  
Request 'https://gimmeip2.azurewebsites.net/' 100 times taking '03.681'. Average 36ms/request.  

That's quite a difference, more than I was expecting. I ran it several times, there was a little variance, but effectively the same outcome.

When reusing the HttpClient instance and making an HTTP request it was nearly twice as fast. Making an HTTPS request was over 4 times faster. I expected there to be some overheads because the HTTP connection is closed between requests, but not such a stark difference on HTTPS.

Why is HTTPS 4 times slower

To find out why HTTPS gave such a disparity, I turned on fiddler, and then it became clear. Here's a snippet of the fiddler trace when disposing every request:

fiddler trace of the disposing client

It shows the SSL handshake occurring every request, whereas when reusing the client (see below), the SSL handshake occurred once, then never again.

fiddler trace of the singleton client

Conclusion

The results show there can be quite a performance improvement gained by reusing HttpClient instances.

To be fair my example is contrived. The server is doing nothing, and therefore exaggerating the network latencies of opening new HTTP connections and the SSL handshake. Realistic requests would likely be slower, therefore the network latencies would not be as stark.

Nevertheless, it's easy enough to reuse HttpClient instances, either through static variables for Lazy Initialization. Therefore you may as well get the performance benefit if you can.

If you would like to run the test for yourself I've made the source code available in my GitHub repository httpclient-dispose.

James Skimming

James has been developing professional software for 20 years, and unprofessionally before that.

United Kingdom https://skimming.net/