P3.NET

HttpClient–Is It Really Thread-Safe?

HttpClient is the recommended way to make calls to web APIs in .NET. But it has some high startup costs. Microsoft recommends that the client be created once and reused throughout the life of a program. In modern applications we have multiple threads going at the same time so the question comes up “is it thread-safe”. The documentation says yes but having used it in multi-threaded code I was not so sure so I dug through the code to see if it really is. What I found is that it is – mostly.

Thread Safety

People can have different opinions of what thread-safe means so let’s clarify the definition I’ll use. To be thread-safe means a single instance of the type can be used on different threads without any of the threads changing the way the instance is working in other threads. Specifically I’m not referring to whether changing something in one thread would cause an error in another but rather it would potentially change the behavior. In general, setting properties is thread-safe because .NET uses atomic writes but the impact of changing the value may alter how other threads behave.

In the .NET world this will generally imply the following.

  1. Thread-safe members can only rely on data passed as arguments.
  2. Thread-safe members can rely on shared (instance or static) data only if the data is immutable.
  3. Thread-safe members can rely on shared (instance or static) data that is mutable only if locking mechanisms are used.
  4. A special case is made for data that is thread-safe for creation (i.e. Lazy). Such data can be considered immutable provided the code guarantees that the creation is thread-safe.
  5. Thread-safe members can only call other thread-safe members.

Let’s explain this by way of example.

class SafeType
{
    public SafeType ()
    {
        SetOnceValue = 10;
    }

    public int NotSafeValue { get; set; }

    public static int SafeSharedValue => 10;

    public int SetOnceValue { get; private set; }

    public void FooSafe ( string value )
    {
        //A - Thread safe as this is call stack data
        var useValue = value;

        //B - Thread safe as this is immutable data
        var useShared = SafeSharedValue;

        //C - Not thread safe if this is changed by other threads 
        //and its value impacts the method implementation
        var useNotSafe = NotSafeValue;

        //D - Not thread safe
        FooNotSafe();

        //E - Immutable after creation, but would need
        //to verify this value is never changed outside of ctor
        var useSetOnceValue = SetOnceValue;
    }

    public void FooNotSafe ( )
    {
        //Not thread safe
    }
}
  • A: Using the argument is thread-safe because the stack is per thread. But if this were a complex type then the argument’s data may still be changed by other threads.
  • B: The static field is immutable and therefore thread-safe.
  • C: This is relying on a mutable instance field. It is not thread-safe by nature but could be thread-safe in the current implementation depending upon how it is used. But future changes to the code may make it suddenly not thread-safe.
  • D: Calling a member that is not considered thread-safe is unsafe. As with C, it may work now but could break later with new code changes.
  • E: Using a mutable data member that is only set at construction is technically thread-safe. But as with the other scenarios it is possible the code may change later to make it unsafe. If the field were marked readonly this would ensure it was mutable until creation.

HttpClient Issues

Now let’s look at HttpClient to see if it meets the criteria. It is always good to start with the properties. There are 3 that are mutable and therefore not thread-safe.

BaseAddress impacts what URL is called. Changing this in different threads would impact the calls made by other threads. This is most certainly not thread-safe.

MaxResponseContentBufferSize determines how much data can be read in a single response. Unless you are downloading large files (in which case you’d up this value) it is unlikely you’d ever lower this value but if you did it may break other threads trying to download large sets of data. So this is not thread-safe but it is unlikely to impact other threads if changed.

Timeout determines how long to wait for the response. It is reasonable that this may be changed to a larger value for calls that may take a while. This would impact other threads but probably not in a breaking way.

The other property of interest on this type is DefaultRequestHeaders. It cannot be set but it is not immutable. It is likely that different calls would want to pass different request headers. But the list of headers defined here are shared by all requests. This property is not thread-safe. Imagine that you’re calling an API that wants to page data back to you. A common approach is to use the headers to specify what is available. Some APIs expect you to pass headers back for subsequent pages of data. Trying to request multiple pages on different threads at the same time would fail as the headers would overwrite each other. HttpClient lacks a thread-safe way to pass per-request headers.

Looking back, it seems like only DefaultRequestHeaders and BaseAddress are the troublesome members. Setting the others would likely not cause an issue but look carefully at the possible exceptions. Notice that InvalidOperationException? This is thrown if the client is in the middle of any request and you change a property. This just made changing any of the properties no longer thread-safe. It does this by using a private boolean field to track whether an operation has been started or not. That field gets set on the first request. It never gets reset. So the properties are immutable once the first request starts. This is technically not thread-safe if different threads were trying to initialize the client before making a request. It is likely one will eventually try to set a property after the other thread has started a request.

When It Is Thread Safe

As with most complex types, it is useful to break up the initialization of an instance from its usage. This is how HttpClient is really designed to be used in my opinion. All the properties are really associated with setting up a request. Once the initialization is done then the properties should not be altered again. In this situation the type is thread-safe. Ideally this type should have been designed to force you into the bucket of success. It should have been clear that initialization is not thread-safe. This could have been done using a factory or even accepting all the data as part of the constructor. Instead you simply have to understand the implementation details of the type.

To make the usage of this type thread-safe you should do the following.

  1. Ensure the creation and initialization of the client is done on a single thread, preferably through a factory.
  2. Ensure that no code outside the initialization tries to modify any of the properties.
  3. Create a new client for each unique base address and request header combinations.

Note that the last point goes against the general rule of 1 instance of the client. It should be 1 instance per unique set. This, of course, makes it harder to enforce in code.

HttpClientManager

To help push code into the bucket of success I created a factory class for creating HttpClient instances. NOTE: This is very much beta-level of code. I’m using it in some production code without issues but it hasn’t been fully tested. This is a snippet of the full class.

public static class HttpClientManager
{
    public static void Clear ()
    { 
        lock (s_clients)
        {
            foreach (var client in s_clients.Values)
            {
                SafeDispose(client);
            };

            s_clients.Clear();
        };            
    }

    public static bool Exists ( string clientName )
    {
        Verify.Argument(nameof(clientName)).WithValue(clientName).IsNotNullOrEmpty();

        return s_clients.ContainsKey(clientName);
    }

    public static void Remove ( string clientName )
    {
        Verify.Argument(nameof(clientName)).WithValue(clientName).IsNotNullOrEmpty();

        if (s_clients.TryRemove(clientName, out HttpClient client))
            SafeDispose(client);
    }
        
    private static HttpClient CreateCore ( Uri clientUri, HttpMessageHandler handler )
    {
        var client = (handler != null) ? new HttpClient(handler) : new HttpClient();
        client.BaseAddress = clientUri;

        return client;
    }

    private static HttpClient GetCore ( string clientName, Func<HttpClient> creator )
    {
        return s_clients.GetOrAdd(clientName, s => creator());
    }

    private static void SafeDispose ( HttpClient client )
    {
        try
        {
            if (client != null)
                client.Dispose();
        } catch
        { /* Ignore exceptions */ };
    }

    private static readonly ConcurrentDictionary<string, HttpClient> s_clients = new ConcurrentDictionary<string, HttpClient>();
}

Since trying to track base addresses and request headers is complicated I’ve gone the route of using a unique client name to identify a client. The client name is determined by the calling code. Given a client name the manager will look to see if the client has been created yet. If it hasn’t then it is created, added to a list of available clients and then returned. It is up to the calling code to determine how to separate clients based upon the requests being sent. The factory class is simply a lifetime manager. To allow for the various permutations of construction there are multiple overloads available. For more complex initialization the calling code can pass a function to create and initialize the client. The factory doesn’t attempt to hide the HttpClient instance from the calling code. Poorly written code could still use one of the mutable properties and fail but it does try to separate initialization from usage. Here’s a sample of how it could be used.

var client = HttpClientManager.Get("ValuesApi", "http://tempuri.org/api/values");
client.GetAsync("");

The biggest issue is the lifetime of the clients. In the current implementation the clients are kept for the life of the application. The code was written for web application that run for a while and then stop. For long running processes this wouldn’t be a good idea. The code in GitHub uses a MemoryCache implementation.

Download the code on Github.