How to change the HttpClient timeout per request in C#

By FoxLearn 1/21/2025 9:32:01 AM   37
When making multiple requests with a shared HttpClient instance, you may sometimes need to modify the timeout for specific requests.

The best way to achieve this is by utilizing a CancellationToken, which allows you to set a custom timeout for each individual request without affecting the overall timeout of the HttpClient.

using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
{
    try
    {
        var response = await httpClient.GetAsync(uri, tokenSource.Token);
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return content;
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("Request timed out");
    }
}

You can’t modify HttpClient.Timeout once the instance is in use. Instead, you can use a CancellationToken to handle per-request timeouts. The actual timeout used is the smaller of HttpClient.Timeout and the timeout specified by the CancellationToken.

For example:

  • If HttpClient.Timeout is 10 seconds, but you set the CancellationToken timeout to 3 seconds, the timeout will be 3 seconds.
  • If you set the CancellationToken timeout to 12 seconds, the timeout will be 10 seconds (as it is the smaller value).

If you attempt to modify HttpClient.Timeout after the instance has been used for the first request, you'll encounter the following exception:

InvalidOperationException: This instance has already started one or more requests and can only be modified before sending the first request.

    When controlling timeouts, remember that since you can't change HttpClient.Timeout after the instance is used, you cannot set it to a value larger than HttpClient.Timeout. Therefore, if you are using CancellationTokens for per-request timeouts, ensure that HttpClient.Timeout is set to a value greater than any maximum timeout you intend to use. (By default, HttpClient.Timeout is 100 seconds.)

    CancellationToken Timeout Less than HttpClient Timeout

    static async Task Main(string[] args)
    {
        string uri = "https://localhost:5000/stocks/VTSAX";
    
        var requestTimeout = TimeSpan.FromSeconds(1);
        var httpTimeout = TimeSpan.FromSeconds(5);
    
        HttpClient httpClient = new HttpClient();
        httpClient.Timeout = httpTimeout;
    
        var stopwatch = Stopwatch.StartNew();
    
        try
        {
            using (var tokenSource = new CancellationTokenSource(requestTimeout))
            {
                var response = await httpClient.GetAsync(uri, tokenSource.Token);
            }
        }
        catch (TaskCanceledException)
        {
            Console.WriteLine($"Timed out after {stopwatch.Elapsed}");
        }
    }

    This indicates the request used the 1-second timeout specified by the CancellationToken.

    CancellationToken Timeout Greater than HttpClient Timeout

    Now, change the CancellationToken timeout to a greater value:

    var requestTimeout = TimeSpan.FromSeconds(10);
    var httpTimeout = TimeSpan.FromSeconds(5);

    This shows that the request used HttpClient.Timeout, which was 5 seconds, as the CancellationToken timeout was greater.

    Avoid Invalid Timeouts

    When creating a CancellationTokenSource, be cautious not to pass invalid timeout values. A timeout of 0 will cause an immediate timeout:

    new CancellationTokenSource(TimeSpan.FromSeconds(0));

    If you pass a negative timeout, you’ll encounter an exception:

    new CancellationTokenSource(TimeSpan.FromSeconds(-1));

    This will throw:

    System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter ‘delay’)

    To avoid unexpected behavior, you should verify the timeout value is valid before creating the CancellationTokenSource:

    if (requestTimeout.TotalSeconds > 0)
    {
        using (var tokenSource = new CancellationTokenSource(requestTimeout))
        {
            var response = await httpClient.GetAsync(uri, tokenSource.Token);
            response.EnsureSuccessStatusCode();
            var content = await response.Content.ReadAsStringAsync();
            return content;
        }
    }

    In cases where you want the user to be able to cancel the request manually while also specifying a custom timeout, you can combine multiple CancellationToken instances using CancellationTokenSource.CreateLinkedTokenSource().

    public async Task<string> FetchDataAsync(string uri, TimeSpan timeout, CancellationToken userCancellationToken)
    {
        try
        {
            using (var timeoutCts = new CancellationTokenSource(timeout))
            using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, userCancellationToken))
            {
                var response = await httpClient.GetAsync(uri, linkedCts.Token);
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
        catch (TaskCanceledException)
        {
            if (userCancellationToken.IsCancellationRequested)
            {
                Console.WriteLine("Operation canceled by user.");
            }
            else
            {
                Console.WriteLine("Request timed out.");
            }
            throw;
        }
    }

    This method allows you to handle both the timeout and user cancellation gracefully, distinguishing between the two causes when either occurs.

    Sometimes HttpClient.Timeout might seem to be ignored if automatic proxy detection is slow. Disabling proxy detection can resolve this:

    In .NET Framework, disable proxy detection in the app.config or web.config

    <system.net>
        <defaultProxy>
            <proxy bypassonlocal="true" usesystemdefault="false" />
        </defaultProxy>
    </system.net>

    In .NET Core, disable the proxy programmatically:

    var handler = new HttpClientHandler { UseProxy = false };
    var httpClient = new HttpClient(handler);