Since HttpClient was released back in 2012 it's become the recommended way of talking to the web from C#. It follows the HTTP standard very closely and has very good support for the async programming model. During my years working in Services I've seen it used a lot. Sometimes people hit the same problems.
Here are some tips on the best way of using it in a service that you expect will be running 24/7.
- IFF you create a new HttpClient per request you should Dispose it (but don't create a new HttpClient per request).
The Dispose method is where the HttpClient cleans up the HttpMessageHandler. So if you don’t Dispose it and you keep creating new HttpClients per request you will eventually get out of resources. I recommend the "using" pattern to do this automatically for you.
- Do NOT recreate HTTPClient for each request. Reuse Httpclient as much as possible
When you create a new HttpClient to make a request there is a lot of overhead. A new connection will have to be established for each request so you'll have your Three-way handshake and if you are going through HTTPS the cost will be even higher. If you are interested in understanding services performance I recommend a lot that you take a look at "High Performance Browser Networking".
Httpclient was written so that you would keep it alive for as long as your application is alive. The methods on it are threadsafe and it will save you the latency cost of establishing a new connection for each request. And you don't even need to worry about adding locking primitives to it. It just works!
This is particularly important in services because you might be paying this cost for each customer request. So even though in your tests it doesn’t seem like the impact is a lot, once you deploy you will see this impact. I’ve actually seen profiles that indicate 5-7% CPU usage just spent on tearing down the HttpClients (this is a lot!).
Making it static is fine. This might not always be possible because you might need different properties but you should try to do it for related requests.
You can also use something like ConnectionGroupName to group multiple requests.
- Use timeouts to control the behavior of your requests
Finding out if a connection was established is normally very quick. But if there is a network partition it might take longer. If you are making cross datacenter calls your latency might be even higher. Depending on what your service does, the default value of
HttpClient.TimeOut might not be what you want so make sure you add the right value to it.
- Reuse connections when you can (httpClient.DefaultRequestHeaders.ConnectionClose = false or HttpWebRequest.KeepAlive = true)
This setting controls if HttpClient will close the connection to the service after each request. "Obviously" this is not what you want to do so make sure you have set it to false.
However some services might actually ask you to close the connection after each request. In particular if your request goes through a load balancer by keeping the connection alive your requests will not go through the load balancer so you might end up getting throttled. I've seen this with some Azure Services. If that is the case see the following recommendation
- Don't hold those connections forever
If you set Keepalive to true then those connections will be held forever. If the backend machine is down it might take a long while for your service to realize that is the case (and during this time your service will not be available). Also as mentioned before this does not play very well with LoadBalancers.
But there is a setting that allows the client to close the active connection after some time has passed and refresh the connection to the backend. This is exactly what we want.
ServicePointManager.FindServicePoint(new Uri(url)).ConnectionLeaseTimeout = ConnectionLeaseTimeoutMs;
- Error management
HttpClient tries very had to not throw. This allows you to examine the Response Status Code and based on that log the result, retry, backoff, etc.
One exception of this is when there is a TimeOut. In this case The HttpClient will throw a TaskCanceledException so you can catch that and do what you need.