4

I have an app that's sending 500 HTTP requests asynchronously. All requests processed after 15 seconds fail because of the timeout on the HTTP Client, even when the requested endpoint already returned a 200 OK.

The code is very straight-forward. Here we take a chunk of requests (500), and execute them asynchronously. It should be noted that the function below is an Azure function running on the consumption-based plan.

    public async Task RunBatch(List<Request> requests)
    {
        if (requests != null && requests .Count > 0)
        {
            var tasks = new Task[requests.Count];
            var i = 0;
            foreach (var request in requests)
            {
                var request = new HttpRequestMessage(HttpMethod.Post, new Uri(request.Url));
                request.Content = new StringContent(request.BodyString, Encoding.UTF8, "application/json");
                tasks[i] = _httpClient.SendAsync(request);
                i++;
            }

            await Task.WhenAll(tasks);
        }
    }

The following code exists in my constructor

_httpClient = new HttpClient();
_httpClient.Timeout = new TimeSpan(0, 0, 15); // 15 seconds

Here are logs from Azure.

enter image description here enter image description here

I'd like each request to have a timeout of 15 seconds. However, I need it to give me an accurate response code whenever my server gets around to processing the awaited request. Is this possible?

I should note: with a higher Http timeout (1 min), all requests succeed.

3
  • 1
    500 concurrent requests is a lot for a single client to handle. You'll need to be very careful with threading to ensure your async tasks can get enough CPU time to avoid triggering a timeout. Commented Feb 3, 2020 at 4:22
  • can you share code? Looks like you are calling async in loop. Commented Feb 3, 2020 at 4:42
  • Added code. In this case, does it make sense to use a separate HTTP Client for batches of 20 or something similar? That would require opening lots of ports so I'm guessing not. Commented Feb 3, 2020 at 16:02

5 Answers 5

11
+50

Personally, I think attempting to issue 500 concurrent requests is always going to be error prone. You mention that you're doing it asynchronously, but in reality there's not a whole lot of asynchrony in your code as you fire-up 500 "hot" tasks then wait for them all to finish.

I would use a semaphore to control how many requests can be made at once. You may have to play with the numbers to find the sweet spot.

The following code works well in LINQPad (although bing quickly notices the odd number of requests and starts adding a CAPTCHA to the page):

// using System.Threading;
async Task Main()
{
    var httpClient = new HttpClient();
    var urls = Enumerable.Range(1, 500).Select(e => "https://www.bing.com/").ToList();
    
    // 10 concurrent requests - tweak this number
    var semaphore = new SemaphoreSlim(10, 10);
    
    var tasks = urls.Select(u => MakeRequest(u, semaphore, httpClient));
        
    var allResponses = await Task.WhenAll(tasks);
    
    // Do something with allResponses
}

private async Task<string> MakeRequest(string url, SemaphoreSlim semaphore, HttpClient httpClient)
{
    try
    {
        await semaphore.WaitAsync();
        var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
        var response = await httpClient.SendAsync(request);
        
        // Add an optional delay for further throttling:
        //await Task.Delay(TimeSpan.FromMilliseconds(100));
        
        return await response.Content.ReadAsStringAsync();
    }
    finally
    {
        semaphore.Release();
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Paul - thanks for your response. I had never heard of SemaphoreSlim before. Though it's probably not what I'm looking for as I'm really trying to execute as many HTTP Requests at one time as possible. My app allows for requesting an HTTP request at a later time, so in periods of high load, there could be thousands going out. I'm currently dealing with this by spinning up serverless Azure Functions that process batches of 500. Maybe my only option is to reduce the amount being processed per batch.
I suspect it is what you want as it applies the level of throttling you need. When responses are being received quickly, then the next requests are issued quickly; however, if the responses are not received quickly it won't keep piling on new requests. This way your total number of requests get dealt with as quickly as possible. Seems a good fit to me!
I agree with Paul, it's a good fit. I've just solved a similar task using SemaphoreSlim. In my case, I wanted to process 200 orders (processing includes several requests to DB and third-party API). Initially in my test that processing failed by the same timeout after 2 minutes, with throttling its runtime is 15 seconds with no issues.
4

There are a few problem of HttpClient.

For example:

  1. HttpClient is disposable: Using HttpClient with the using statement is not the best choice because even when you dispose HttpClient object, the underlying socket is not immediately released and can cause a serious issue named ‘sockets exhaustion’. For more information about this issue, You're using httpclient wrong and it is destabilizing your software

  2. HttpClient is intended to be instantiated once and reused throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. That issue will result in SocketException errors. Possible approaches to solve that problem are based on the creation of the HttpClient object as singleton or static.

  3. HttpClient that you can have when you use it as singleton or static object. In this case, a singleton or static HttpClient doesn't respect DNS changes. For more information :Singleton HttpClient doesn't respect DNS changes

To address those mentioned issues and make the management of HttpClient instances easier, .NET Core 2.1 introduced a new HttpClientFactory.

What is HttpClientFactory:

  1. Provide a central location for naming and configuring logical HttpClient objects. For example, you may configure a client (Service Agent) that's pre-configured to access a specific microservice.
  2. Codify the concept of outgoing middleware via delegating handlers in HttpClient and implementing Polly-based middleware to take advantage of Polly’s policies for resiliency.
  3. HttpClient already has the concept of delegating handlers that could be linked together for outgoing HTTP requests. You register HTTP clients into the factory and you can use a Polly handler to use Polly policies for Retry, CircuitBreakers, and so on.
  4. Manage the lifetime of HttpClientMessageHandlers to avoid the mentioned problems/issues that can occur when managing HttpClient lifetimes yourself.

For more details please visit this link: Use HttpClientFactory to implement resilient HTTP requests

You can also use RestSharp library for send any requests. For more http://restsharp.org/

Referenced resources:

  1. https://josefottosson.se/you-are-probably-still-using-httpclient-wrong-and-it-is-destabilizing-your-software/
  2. https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
  3. https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
  4. https://github.com/dotnet/runtime/issues/18348

Comments

1

A few things I would be checking:

  • Rather than use a new HttpClient each time, use an HttpClientFactory to avoid the overhead of HttpClient construction, and the exhaustion of ports.

  • Are you sure that you're not being rate-limited by Bing? Try targeting a remote server under your control.

  • Consider limiting the concurrency of your requests with a SemaphoreSlim or a parallelism library.

Comments

0

Try to increase default connections limit:

ServicePointManager.UseNagleAlgorithm = true;
ServicePointManager.Expect100Continue = true;
ServicePointManager.DefaultConnectionLimit = <number>;

1 Comment

I updated my question with a second image which shows more clearly the timing of the issue. On multiple runs, there is no consistent number of failures. I don't believe this has to do with the number of connections.
0

Seems you are sending request to bing search engine right? If I'm right you have another problem. Bing search may be doesn't allow bot requests.hence bing block requests.check further about that.You can try with some time delays instead of async requests.your app seems working fine.You can verify my theory by sending requests to another service or API.

2 Comments

Thanks for the response. I should've noted, if I increase the HTTP Timeout to 1 min+, all the requests succeed.
@BrandonMcAlees - 1 min doesn't invalidate this answer. Maybe bing is ok with this setting. Have you tried on a server you own?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.