How to send many requests in parallel in ASP.Net Core

I want to make 1000 requests! How can I make it really fast? Let’s have a look at 4 approaches and compare their speed.

Preparations

In order to test different methods of handling requests, I created a very simple ASP.Net Core API, that return user by his id. It fetches them from plain old MSSQL database.

I deployed it quickly to Azure using App services and it was ready for testing in less than two hours. It’s amazing how quickly a .net core app can be deployed and tested in a real hosting environment. I was also able to debug it remotely and check it’s work in Application Insights.

Here is my post on how to build an app and deploy it to Azure: https://www.michalbialecki.com/2017/12/21/sending-a-azure-service-bus-message-in-asp-net-core/

And a post about custom data source in Application Insights: https://www.michalbialecki.com/2017/09/03/custom-data-source-in-application-insights/

API in a swagger looks like this:

So the task here is to write a method, that would call this endpoint and fetch 1000 users by their ids as fast as possible.

I wrapped a single call in a UsersClient class:

    public class UsersClient
    {
        private HttpClient client;

        public UsersClient()
        {
            client = new HttpClient();
        }

        public async Task<UserDto> GetUser(int id)
        {
            var response = await client.GetAsync(
                "http://michalbialeckicomnetcoreweb20180417060938.azurewebsites.net/api/users/" + id)
                .ConfigureAwait(false);
            var user = JsonConvert.DeserializeObject<UserDto>(await response.Content.ReadAsStringAsync());

            return user;
        }
    }

#1 Let’s use asynchronous programming

Asynchronous programming in C# is very simple, you just use async / await keywords in your methods and magic happens.

    public async Task<IEnumerable<UserDto>> GetUsersSynchrnously(IEnumerable<int> userIds)
    {
        var users = new List<UserDto>();
        foreach (var id in userIds)
        {
            users.Add(await client.GetUser(id));
        }

        return users;
    }

Score: 4 minutes 51 seconds

This is because although it is asynchronous programming, it doesn’t mean requests are done in parallel. Asynchronous means requests will not block the main thread, that can go further with the execution. If you look at how requests are executed in time, you will see something like this:

Let’s run requests in parallel

Running in parallel is the key here because you can make many requests and use the same time that one request takes. The code can look like this:

    public async Task<IEnumerable<UserDto>> GetUsersInParallel(IEnumerable<int> userIds)
    {
        var tasks = userIds.Select(id => client.GetUser(id));
        var users = await Task.WhenAll(tasks);

        return users;
    }

WhenAll is a beautiful creation that waits for tasks with the same type and returns a list of results. A drawback here would be an exception handling because when something goes wrong you will get an AggregatedException with possibly multiple exceptions, but you would not know which task caused it.

Score: 28 seconds

This is way better than before, but it’s not impressive. The thing that slows down the process is thread handling. Executing 1000 requests at the same time will try to create or utilize 1000 threads and managing them is a cost. Timeline looks like this:

Let’s run requests in parallel, but smarter

The idea here is to do parallel requests, but not all at the same time. Let’s do it batches for 100.

    public async Task<IEnumerable<UserDto>> GetUsersInParallelFixed(IEnumerable<int> userIds)
    {
        var users = new List<UserDto>();
        var batchSize = 100;
        int numberOfBatches = (int)Math.Ceiling((double)userIds.Count() / batchSize);

        for(int i = 0; i < numberOfBatches; i++)
        {
            var currentIds = userIds.Skip(i * batchSize).Take(batchSize);
            var tasks = currentIds.Select(id => client.GetUser(id));
            users.AddRange(await Task.WhenAll(tasks));
        }
            
        return users;
    }

Score: 20 seconds

This is the slightly better result because framework needs to handle fewer threads at the same time and therefore it is more effective. You can manipulate the batch size and figure out what is best for you. Timeline looks like this:

The proper solution

The proper solution needs some modifications in the API. You won’t always have the ability to change the API you are calling, but only changes on both sides can get you even further. It is not effective to fetch users one by one when we need to fetch thousands of them. To further enhance performance we need to create a specific endpoint for our use. In this case – fetching many users at once. Now swagger looks like this:

and code for fetching users:

    public async Task<IEnumerable<UserDto>> GetUsers(IEnumerable<int> ids)
    {
        var response = await client
            .PostAsync(
                "http://michalbialeckicomnetcoreweb20180417060938.azurewebsites.net/api/users/GetMany",
                new StringContent(JsonConvert.SerializeObject(ids), Encoding.UTF8, "application/json"))
            .ConfigureAwait(false);

        var users = JsonConvert.DeserializeObject<IEnumerable<UserDto>>(await response.Content.ReadAsStringAsync());

        return users;
    }

Notice that endpoint for getting multiple users is a POST. This is because payload we send can be big and might not fit in a query string, so it is a good practice to use POST in such a case.

Code that would fetch users in batches in parallel looks like this:

    public async Task<IEnumerable<UserDto>> GetUsersInParallelInWithBatches(IEnumerable<int> userIds)
    {
        var tasks = new List<Task<IEnumerable<UserDto>>>();
        var batchSize = 100;
        int numberOfBatches = (int)Math.Ceiling((double)userIds.Count() / batchSize);

        for (int i = 0; i < numberOfBatches; i++)
        {
            var currentIds = userIds.Skip(i * batchSize).Take(batchSize);
            tasks.Add(client.GetUsers(currentIds));
        }
            
        return (await Task.WhenAll(tasks)).SelectMany(u => u);
    }

Score: 0,38 seconds

Yes, less than one second! On a timeline it looks like this:

Comparing to other methods on a chart, it’s not even there:

How to optimize your requests

Have in mind, that every case is different and what works for one service, does not necessarily need to work with the next one. Try different things and approaches, find methods to measure your efforts.

Here are a few tips from me:

  • Remember that the biggest cost is not processor cycles, but rather IO operations. This includes SQL queries, network operations, message handling. Find improvements there.
  • Don’t start with parallel processing in the beginning as it brings complexity. Try to optimize your service by using hashsets or dictionaries instead of lists
  • Use smallest Dtos possible, serialize only those fields you actually use
  • Implement an endpoint suited to your needs
  • Use caching if applicable
  • Try different serializers instead of Json, for example ProfoBuf
  • When it is still not enough… – try different architecture, like push model architecture or maybe actor-model programming, like Microsoft Orleans: https://www.michalbialecki.com/2018/03/05/getting-started-microsoft-orleans/

You can find all code posted here in my github repo: https://github.com/mikuam/Blog.

Optimize and enjoy 🙂

32 thoughts on “How to send many requests in parallel in ASP.Net Core

  1. Random dude on the internet

    You may want to check out ServicePointManager.DefaultConnectionLimit, since your requests are to the same point they are most likely only 2 at a time actually running. This is most likely the reason for ~20s to run them, not the thread and context switches because those usually take microseconds. That’s assuming your api can handle many concurrent requests.

    Reply
  2. Buddy from behind the desk

    “… A drawback here would be an exception handling because when something goes wrong you will get an AggregatedException with possibly multiple exceptions, but you would not know which task caused it…”

    Actually ‘Task.WhenAll()’ won’t return the AggregateException (you can read here: https://stackoverflow.com/questions/12007781/why-doesnt-await-on-task-whenall-throw-an-aggregateexception and here https://codeblog.jonskeet.uk/2011/06/22/eduasync-part-11-more-sophisticated-but-lossy-exception-handling/). Short explanation is that:
    “…Now the team in Microsoft could have decided that really you should catch AggregateException and iterate over all the exceptions contained inside the exception, handling each of them separately. (…) They decided to simply extract the first exception from the AggregateException within a task, and throw that instead…”
    So the output of the ‘…await Task.WhenAll()…” will be basic Exception (first task which failed). The way to handle exceptions from other task can be, eg.:
    “…public static async Task AwaitGracefully(this IList taskList, Action logger)
    {
    try
    {
    await Task.WhenAll(taskList);
    }
    catch (Exception)
    {
    foreach (var task in taskList.Where(t => t.IsFaulted))
    {
    logger(task.Exception?.InnerException ?? task.Exception);
    }
    }
    }
    …”

    Reply
  3. hatchsandwich.com

    I am really impressed with your writing skills and also with
    the layout on your blog. Is this a paid theme or did you customize it yourself?

    Either way keep up the excellent quality writing, it’s rare to see a nice blog like this one these days.

    Reply
  4. ssinfod

    Hello, would it be possible to add the code of the visual studio project on your github ?
    I would like to see the UserDto.cs file.

    thanks for the great article.

    Reply
  5. Saeid

    In your last example you modify your Api to support return list of users instead returning single user. Actually this is not our concern in this case because we might don’t access to modify Api, It maybe another vendor Api and we can not access it.

    Reply
    1. Michał Białecki Post author

      Saeid,
      That’s correct. In this example, I wanted to show, that speeding up communication between micro-services can be done on one side just to a certain point. In order to go a step forward, both sides need to be involved.
      Thanks for feedback 🙂

      Reply
  6. Marko

    In the case of parallel execution where we don’t do batches, and send 1000 requests in parallel, do we have a guarantee do receive responses in the same order we sent the requests?

    Reply
  7. Peter

    Hello,

    Thank you very much for your article, it helps me a lot.

    I’ve implemented the “Let’s run requests in parallel, but smarter”, but I have a problem :

    Here is my code :
    var tasks = currentIds.Select(product => client.PutAsync(“url”, “json”, Encoding.UTF8, “application/json”)));
    response = await Task.WhenAll(tasks);

    But I do need associate each response to each product, I co not arrived to do this.

    Actually I’m just having a list of response, but I do not know which product is concerned for each response.

    Thank you

    Reply
    1. Michał Białecki Post author

      Maciej,
      It’s a very good point. However, the full answer would have a small ‘but’. In ASP.Net Core there’s no `SynchronizationContext`, so there’s no point in using `ConfigureAwait(false)`, however, it is advised to use when writing a library, that could be used in old ASP.Net.

      Have a look here for more info.

      Reply
  8. Max

    Thanks very much, I’ve modified your code a bit to make it a generic extension and added an optional delay in between batches.

    Reply
  9. Max

    Thanks very much, I’ve modified your code a bit to make it a generic extension and added an optional delay in between batches.
    “`c#
    public static class TaskExtentions
    {
    // https://www.michalbialecki.com/2018/04/19/how-to-send-many-requests-in-parallel-in-asp-net-core/
    public async static Task<IEnumerable> WhenAllInBatches(this Task task, IEnumerable<Task> tasks, int batchSize, int? batchPauseMiliSeconds = null )
    {
    var results = new List();
    int numberOfBatches = (int)Math.Ceiling((double)tasks.Count() / batchSize);

    for (int i = 0; i < numberOfBatches; i++)
    {
    var currentIds = tasks.Skip(i * batchSize).Take(batchSize);
    results.AddRange(await Task.WhenAll(tasks));
    if (batchPauseMiliSeconds.HasValue) await Task.Delay(batchPauseMiliSeconds.Value);
    }

    return results;
    }

    }
    “`

    Reply
    1. maxymus

      oops should be

      for (int i = 0; i < numberOfBatches; i++)
      {
      var currentBatch = tasks.Skip(i * batchSize).Take(batchSize);
      results.AddRange(await Task.WhenAll(currentBatch));
      if (batchPauseMiliSeconds.HasValue) await Task.Delay(batchPauseMiliSeconds.Value);
      }

      Reply
  10. Anas

    Many thanks ,
    But i got errors when i tried to use this code with EntityFramework core :
    System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext

    Is there any workaround for this error ?
    Best regards

    Reply
  11. Alexander

    Hi, this is a great article, helped me a lot.
    I have just one question, about the last part.
    This PostAsync looks like the method is calling itself, no? Or am I missing somethig.
    To me it looks like this is the method GetMany.
    Is the flow this:
    http://michalbialeckicomnetcoreweb20180417060938.azurewebsites.net/api/users/GetMany – this is the method that executes in paralel
    And public async Task<IEnumerable> GetUsers(IEnumerable ids) – is the async method that calls the method that executes in paralel?
    Somehow this looks to me like a double job.

    public async Task<IEnumerable> GetUsers(IEnumerable ids)
    {
    var response = await client
    .PostAsync(
    “http://michalbialeckicomnetcoreweb20180417060938.azurewebsites.net/api/users/GetMany”,
    new StringContent(JsonConvert.SerializeObject(ids), Encoding.UTF8, “application/json”))

    Reply
    1. Michał Białecki Post author

      Thanks Franco,
      The best batchSize will be different in every application. You need to test and check what would be the right value for you. 100 is just an example that worked for me.

      Reply
  12. Jim

    Nice one!

    I, also, have some question. I have to call some SOAP service around 250k times. When I set batchSize to, for example 100, I got all kinfs of errors, something like: “Authentication failed because the remote party has closed the transport stream…”, or “An error occurred while sending the request. The response ended prematurely…” and so on..

    If I set batchSize to some small number like 10, everything is fine, but it is slow.

    What is the best approach in that kind of situation?

    Reply
    1. Rui Vaz

      You’re probably being throttled by the service you are calling. It is something that wasn’t mentioned in this post. Making a ton of calls simultaneously to a service is unfeasible in many scenarios, since some services are not able to handle the load. You would have to either implement a retry mechanism or lower the batch size to something that the service can handle.

      What i think is that the 3rd option will still spam the sh#t out of the serivce, so you are left with the 2nd option, which batches in intervals.

      Reply
  13. Sachin

    Hi Michał ,

    This is really good article. Thanks for sharing your knowledge.
    I have just begun to work or parallel request and async programming and have some doubts.

    1. If I have a service to return just one user from ID, and I have multiple concurrent request coming to the server for the same API. what will be the best approach to handle multiple concurrent requests along with async and await ?
    2. Is it advisable to use parallel for with webapi to utilize all the cores of the processor, when I again have multiple request coming on that API?

    Reply
    1. Michał Białecki Post author

      Hi Sachin,
      I’m not very familiar with the case you are describing, but I’ll try to answer as well as I can.
      1. Web API in .NET Core works very well with many requests in parallel. Even if running the same code, it can be run by multiple threads at the same time. If you use async/await, which is a good practice your code will be even more parallel friendly. With async/await the processing of a request can switch from a thread to thread, so they are used more optimal.
      2. The ability to work in parallel depends on the common parts of the service. If you’re calling the same database and reusing the same db connection, this is something that requests will use one by one. However, writing API so it can work in parallel is a natural thing so you should only consider modification when there is a performance issue.

      Reply
  14. Hoàng Vũ

    Very good article. I am a newbie in programming from Vietnamese. I have a question .how I can post 1000 concurrent requests. thank you

    Reply
  15. shirin

    great but i have a question when one of request has failed i want to do to next one

    like try
    {}
    catch(){
    continue
    }

    Reply
  16. Sameer Chachad

    Great efforts Michał, really appreciate your efforts. You have explained in a very simple manner, anyone can understand it. Lucky to have come across this blog 🙂

    I have one doubt about the execution of the multiple APIs parallelly –
    Assume, there are three Async WebAPI Calls made, each taking 2 seconds, 5 seconds and 3 seconds to execute separately. When we run them in parallel, what would be the average “response Time” for executing all the three WebAPIs asynchronously? .

    Hope I was able to convey my question for you to understand.

    Thanks in advance. Warm Regards

    Reply
  17. Zhanta

    Hi Michal

    I know you wrote the article a while back. Have you since tried to use Iasyncenumerable instead of batching the tasks?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *