A common question that I get asked quite often and a point of reasonable confusion for developers is “Should I dispose of HttpClient or not?”
There isn’t one yes or no answer to this. It really does depend on a few key factors. In this post, I’ll try to answer this question with enough code snippets that it’s clear how this works internally.
In .NET Core and if you’re not using HttpClientFactory, generally, the answer is no, share the HttpClient instance in order to avoid socket exhaustion risks. This solution is not without its own problem though. I won’t rehash the finer details here since I’ve covered it previously in my introduction to HttpClientFactory post.
I’m mostly going to focus on the question as it applies when using a HttpClient instance which has been provided by IHttpClientFactory. To explore this we’ll head into the corefx and Microsoft.Extensions code; but we won’t have to go too deep.
If you want to dig into the relevant code yourself:
- The HttpClient code lives in corefx repository.
- The HttpClientFactory code lives in AspNet Extensions repository.
Constructing HttpClient Instances
The HttpClient class derives from HttpMessageInvoker.
HttpMessageInvoker has two constructor overloads. The first constructor requires a HttpMessageHandler to be passed as an argument. The second constructor accepts a HttpMessageHandler and a bool indicating whether that handler should be disposed of by the HttpMessageInvoker. Here is the main constructor:
In lines 11 and 12, its parameters, the handler and the “disposeHandler” bool are stored into two private fields. The other code is not important for this discussion.
The HttpClient class has three constructor overloads. Starting with the most parameter-rich we have:
Like the base HttpMessageInvoker, this takes a HttpMessageHandler and a bool indicating whether the handler should be disposed of. This calls the base HttpMessageInvoker constructor passing along these parameters. This primary constructor also initialises some fields on the HttpClient with default values. The timeout will be set to the default timeout which is 100 seconds. The maximum response buffer size is set using the MaxBufferSize on HttpContent which is int.MaxValue. Finally, a new CancellationTokenSource is created and stored.
The next constructor looks like this:
It takes a single parameter, accepting a HttpMessageHandler. It calls the main constructor passing a value of true for the disposeHandler argument. By default, we can see that in cases where we manually create a HttpClient instance and pass only the handler, it will by default expect to dispose of the underlying handler when its own Dispose method is called. We’ll look at the disposal code later to understand this better. As this overload calls the main constructor, the fields still get initialised with default values as described above.
The final, most basic constructor overload is parameterless and looks like this:
It will by create a new HttpClientHandler instance for the HttpClient to use and pass that into the next constructor which in turn means that the disposeHandler value will be true here too.
To recap; all but the most parameter-rich constructor will be set up to dispose of the HttpMessageHandler. Only in the main constructor overload can be used to explicitly avoid this.
Creation of HttpClient Instances By HttpClientFactory
In the simplest case, when using IHttpClientFactory directly, we can call CreateClient() to ask for a HttpClient instance. We can now take a look at what this does internally. The signature of the CreateClient method on the DefaultHttpClientFactory (which implements IHttpClientFactory), takes a string value which is the name of the client being requested. When using HttpClientFactory “named clients”, this is how we specify which logically configured client we want from the factory. This method throws when the name is null and there is no parameterless version of this method defined on DefaultHttpClientFactory. So how does this work?
There’s an extension method for IHttpClientFactory in HttpClientFactoryExtensions which looks like this:
This is the method that we call when we just want a basic, generalised instance of HttpClient from the factory. It calls the main CreateClient(string name) method using the Options.DefaultName value. Checking the Options.cs file we can see this is set as string.Empty.
public static readonly string DefaultName = string.Empty;
Let’s jump into the main CreateClient method on DefaultHttpClientFactory and see what this does…
One of the main requirements of HttpClientFactory is to manage the handler lifetimes for us and the reason for this is really to manage the lifetime of the underlying connections. Again, you can read more about these benefits in my previous introduction to HttpClientFactory post.
Under the hood, HttpClientFactory keeps track of the available handlers for each logical HttpClient instance and it will reuse a handler unless it has expired. The default expiry time for handlers is two minutes. I’ll explore the internals of that in another blog post if there is sufficient interest. Ultimately in line 8, the call to “CreateHandler(name)” will return either an existing, non-expired handler or potentially a new one if there are no active handlers available.
It’s the next line (line 9) which we really care about in this post. A new HttpClient instance is created.
At this point, I can’t state strongly enough that DefaultHttpClientFactory.CreateClient(…) will always provide a new HttpClient instance. This can be confusing since its easy to tie the connection lifetime issue with HttpClient. In reality, the connection lifetime is more directly tied to the HttpMessageHandler level.
HttpClient is a pretty basic class with many thread-safe methods and some non-thread-safe properties. Before HttpClientFactory the easiest way to avoid the socket exhaustion issue was to share an instance but really it was the handlers we wanted to re-use. Sharing the HttpClient was just an easier way to share the handler instance.
So, HttpClientFactory constructs a new HttpClient instance, passing in the handler returned by the CreateHandler method as the handler argument. Remember, this may be a new handler or it may be an active, existing handler which may be in use by multiple HttpClient instances returned from the factory. It also crucially passes a value of false for the “disposeHandler” argument. It does this because the factory controls the lifetime of HttpClientHandler instances internally. Active handlers will eventually expire and later be disposed of once there are no HttpClient instances holding a reference to them. Therefore, HttpClientFactory does not want to allow the HttpClient instance to control the disposal of the handler. This would lead to problems if other HttpClient instances hold a reference to the same handler.
We’ll see how this all fits together if we take a look at HttpClient disposal.
Disposal of HttpClient Instances
HttpClient derives from HttpMessageInvoker which implements the IDisposable interface. HttpMessageInvoker implements IDisposable because it has a field which holds onto a HttpMessageHandler. This is actually going to be a chain of handlers, at the end of which is a connection pool and some active connections to endpoints.
HttpClient is an example where the often blanket rule of making sure we always dispose of IDisposable objects breaks down.
To be fair it’s not really a problem with the rule but how it has often been described badly in the documentation and the way we as developers tend to interpret it. IDisposable is an indicator that we should consider proper disposal because there are some unmanaged resources involved. In a vast majority of cases, this does mean that we should dispose of it quickly when we’re finished with the object, but it also requires a little consideration of what the unmanaged resource is. It signals that we most likely need to properly call dispose on the object to release any unmanaged resources properly, but this isn’t appropriate 100% of the time.
With the HttpMessageHandler chain – I’ll state this again because this is really important – it’s the connection that is the reason for the inclusion of IDisposable interface. Re-using connections is something we typically actually do want to do in many cases and hence disposing of the handler and its connections should be an informed decision.
When using HttpClientFactory a lot of this complexity is removed. It’s going to manage the connection lifetimes for you. If you can’t use HttpClientFactory then you need to consider the implications and the trade-off of disposing of the handler and closing connections vs. ensuring connections are refreshed often enough to honour DNS changes to redirect to different endpoints for example.
The dispose method on HttpClient overrides the HttpMessageInvoker Dispose method and looks like this:
I’ll mostly skip past the pendingRequestsCts stuff for this post since it’s not entirely relevant. The short summary is that when HttpClient is disposed of any pending requests are going to be cancelled. In general usage scenarios, I doubt this is something that many people will run into. If you’re using a HttpClientFactory provided instance then it’s not likely you’ll share around the HttpClient so one would expect requests are already completed by the time you’d be in a position to dispose of the HttpClient. In shared HttpClient situations, when multiple threads may be using the same instance, this code ensures that all requests are cancelled before disposing of the HttpClient. You’ll likely have bigger problems if you’re sharing an instance that one code path may dispose of while other code is still using it.
It then calls the base Dispose method on HttpMessageInvoker which looks like this…
Only if the “disposeHandler” field has been set to true when constructing the HttpMessageInvoker will anything important actually happen inside of this Dispose method. It’s essentially a no-op otherwise. As the “disposeHandler” flag will be true under many default scenarios the Dispose method on the handler will be called.
However, as we saw that when the DefaultHttpClientFactory calls the HttpClient constructor, it does so explicitly passing false for the “disposeHandler” argument. Therefore with any clients created by DefaultHttpClientFactory, disposing of them manually is redundant and quite unnecessary. We can treat those instances as if they do not implement IDisposable at all and let them be garbage collected in the normal way. There’s no need to wrap them in a using statement for example.
There is no harm in disposing of them which will be an effective no-op and there are no side-effects which will affect the intended behaviour of HttpClientFactory handler lifetimes. The HttpClientFactory feature has taken responsibility for re-using and also disposing of the handlers its using internally when providing HttpClient instances.
This has been quite a long and technical post for a one-line question! I believe it’s the only way to really answer it with enough information that allows one to understand the behaviour of the various scenarios. As a reminder, the original question we started with was, “Should I dispose of HttpClient or not?”
In short, there are two answers depending on whether you are taking advantage of HttpClientFactory or not. To keep things really simple, use HttpClientFactory whenever you can as things become much simpler.
Answer when using HttpClientFactory:
There is no need to dispose of the HttpClient instances from HttpClientFactory. Disposal will not actually do anything in this case because the factory manages the handler and connection lifetimes and not the HttpClient instances. Calling dispose on HttpClient has no effect for instances provided by the factory and is simply redundant code.
Answer when NOT using HttpClientFactory:
Generally, you don’t want to dispose of HttpClient unless it’s used very infrequently. Regular creation and disposal may lead to socket exhaustion. However, be aware that if you’re never recycling the instance (or the underlying handlers) it means that connections are never automatically closed and DNS changes may not be reflected. In this scenario, there’s no one-size-fits-all solution. This is why using HttpClientFactory should be preferred.