[C#] Why You Should Be Careful With The Task.WhenAll! 😵💀☠️

Do Tran
Level Up Coding
Published in
2 min readMay 9, 2024

--

Asynchronous programming is a powerful technique that has become increasingly popular for building web servers, calling APIs, and sending emails. Due to its ability to handle input/output (I/O) operations asynchronously, it prevents the program from being blocked while waiting for responses. This frees up CPU resources for the web server to perform other tasks concurrently, leading to a significant performance improvement.

But as the saying goes, ‘with great power comes great responsibility.’ Just like a powerful shotgun, asynchronous programming should be used carefully to avoid unintended consequences.

I’m currently working on an e-commerce platform. Recently, I was assigned the task of synchronizing product data with a third-party platform, allowing merchants to sell their products there as well. To illustrate the process, I’ll be presenting a simplified version of the code I’m working on.

var products = await GetAllProductsAsync(); //async, nice :))
var sendTasks = await SendAllProductsAsync(products); //async again
await Task.WhenAll(sendTasks); //all thing async

public async Task SendAllProductsAsync(IEnumerable<Product> products){
var tasks = products.Select(x => SendViaHttpClient(x));
return tasks;
}

Here’s a rephrased version of the text:

There’s a potential issue here. The initial query might retrieve a large number of products. While developers are aware of this and typically avoid it, excessive database calls can be expensive in terms of CPU usage.

The real challenge lies in lines 2 and 3. Here, we send all retrieved products at once using HttpClient. Developers might not be aware that network resources also have limitations. Sending a large volume of data simultaneously can consume significant server resources, including sockets, memory, and most importantly, bandwidth. This can be likened to a self-inflicted DDoS (Distributed Denial-of-Service) attack. A flooded network with millions of tasks triggered by the SendViaHttpClient method could lead to website inaccessibility for users.

The issue extends beyond our own server. A sudden surge of requests to the third-party web server could significantly impact their performance. This could lead to them blocking our service or negatively affect their user experience.

The solution is straightforward. Instead of sending all products simultaneously using Task.WhenAll, we can optimize by sending them in smaller batches. A limit of around 100 products per batch is a good starting point.

using MoreLinq;

var products = await GetAllProductsAsync(); //async, nice :))
var batches = BatchProduct(products, 100); //we use batch = 100 here
foreach(var batch in batches){
await SendAllProductsAsync(batch);
}
}

public async IEnumerable<Batch<Product>> SendAllProductsAsync(IEnumerable<Product> products){
var tasks = products.Select(x => SendViaHttpClient(x));
return tasks;
}

public IEnumerable<Batch<Product>> BatchProduct(IEnumerable<Product> products, int batchNumber){
return products.Batch(batchNumber).ToList();
}

Final thoughts

Asynchronous programming offers significant power, but like any powerful tool, it requires careful consideration to avoid unintended consequences.

--

--