In last few articles, we have been looking at how HttpClient can be used in .NET applications.
We already know that creating new HttpClient instance for every request is not really recommended, as that might exhaust the resources on server. This might result in slow responses from backend HTTP APIs, or even timeouts. But if the HttpClient is singleton (or static), then it would not respect DNS changes.
In this article, let’s have a look at internals of DefaultHttpClientFactory implementation, and how does it try to solve two issues mentioned above.
Resource Exhaustion Issue
This DefaultHttpClientFactory the default implementation of IHttpClientFactory interface. The code for this can be found on GitHub.
If we look at the CreateClient implementation, below is the logic to create HttpClient instance:
- It makes a call to
CreateHandler
. CreateHandler
is responsible for providingActiveHandlerTrackingEntry
instance. But the interesting thing is, these instances are managed as a Dictionary. Key in this dictionary is the name of HttpClient’s logical instance and value isActiveHandlerTrackingEntry
instance.- Note that
CreateHandler
calls GetOrAdd(TKey, Func<TKey,TValue>) method. It returns existing instance ofActiveHandlerTrackingEntry
for the given key. If it is not present, then a new instance is created using Func delegate.
Once the handler to use is decided, it is passed for constructing HttpClient
object. disposeHandler
parameter is set to false, meaning these handlers are maintained in a pool inside IHttpClientFactory
.
What does this mean ? This means that if CreateClient
method is called multiple times to get a named client “github“, every time the HttpClient
instance would be new. But the underlying handlers pipeline instances would be same, thus avoiding resource exhaustion. But HttpClient instance created for named client “googleMapsApi”, would be have different instances of http message handlers. This is applicable to not only named clients but also typed clients.
So, it means that an ActiveHandlerTrackingEntry is created per named client. But how is this related to lifetime management of resources? Let’s first see what is inside an ActiveHandlerTrackingEntry.
Create Handler Entry
This is the method that is called to create a new ActiveHandlerTrackingEntry
instance if it does not exist in the ConcurrentDictionary. This is very complicated method and high level overview of the logic is given below:
- It creates a new scope object, so that scoped object instances can be managed properly
- Next, it gets an instance of DefaultHttpMessageHandlerBuilder which will create an instance of HttpClientHandler. This HttpClientHandler is responsible for creating sockets connection and making the requests.
- Next,
IHttpMessageHandlerBuilderFilter
collection injected in DefaultHttpClientFactory is used to create additionalHttpMessageHandler
s. - Thus a whole pipeline of HttpMessageHandlers is created. This pipeline is then wrapped inside LifetimeTrackingHttpMessageHandler. This handler is just a marker handler which is used while cleaning up the resources.
if you want to know more details about internals, please refer this post from Andrew Lock.
Coming Back to Points
Ok, so coming back to original issues –
- Resource Exhaustion due to many instances of
HttpClient
- Issues for respecting DNS modifications (stale DNS) in case we decide to use single instance of
HttpClient
.
Resource exhaustion issue is solved by pooling the pipelines of handlers. Same handler instances are used for two instances of HttpClient
, as long as they are referring to a same named client.
DNS modifications might lead to stale data if single instance of HttpClient
is used. Stale DNS problems by cycling HttpMessageHandler
instances at regular intervals.
Lifetime
By default, the http message handlers has default lifetime of 2 minutes. So, after every two minutes, the instances of http message hander pipeline is recycled.
Generally, this lifetime duration should be enough for most of the applications. If required, this lifetime duration can be changed via AddHttpClient middleware as shown in below code.
I hope this information is useful. Let me know your thoughts.