Task.WhenAll
Parallel.ForEachAsync
async programming
concurrency
C# performance

Task.WhenAll vs Parallel.ForEachAsync - Which approach is best and why?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

These two APIs solve related but different problems. Task.WhenAll is a coordination primitive for tasks you already created, while Parallel.ForEachAsync is a work-dispatching API that processes an input sequence with built-in concurrency control.

Use Task.WhenAll When the Tasks Already Exist

Task.WhenAll works best when you have a known set of asynchronous operations and want to await their combined completion.

csharp
1using System;
2using System.Net.Http;
3using System.Threading.Tasks;
4
5var client = new HttpClient();
6
7Task<string> a = client.GetStringAsync("https://example.com/a");
8Task<string> b = client.GetStringAsync("https://example.com/b");
9Task<string> c = client.GetStringAsync("https://example.com/c");
10
11string[] results = await Task.WhenAll(a, b, c);
12Console.WriteLine(results.Length);

This is concise and natural when the number of operations is small or already known. Each task starts immediately, and Task.WhenAll simply waits for them all.

The downside is that it is easy to create too many tasks at once. If you project a massive input set into tasks and then await them all, you may create more concurrency than the system or the downstream service can handle.

Use Parallel.ForEachAsync for Bounded Fan-Out

Parallel.ForEachAsync is designed for iterating over a sequence while limiting how many operations run at the same time.

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5var urls = new List<string>
6{
7    "https://example.com/a",
8    "https://example.com/b",
9    "https://example.com/c"
10};
11
12await Parallel.ForEachAsync(
13    urls,
14    new ParallelOptions { MaxDegreeOfParallelism = 2 },
15    async (url, cancellationToken) =>
16    {
17        await Task.Delay(100, cancellationToken);
18        Console.WriteLine(url);
19    });

This is a better fit when you have many items and need the runtime to throttle concurrency automatically.

The Real Decision: Workload Shape

The best choice depends less on micro-benchmarks and more on the shape of the workload.

Choose Task.WhenAll when:

  • you already have the tasks
  • the count is reasonable
  • you want the natural array of results back

Choose Parallel.ForEachAsync when:

  • you have an input collection rather than pre-created tasks
  • you need bounded concurrency
  • each item is processed more for side effects than for collecting return values

That is why asking which one is universally "best" misses the point. Each API is optimized for a different pattern.

Result Handling and Error Behavior

Task.WhenAll naturally returns results when the tasks produce values. That makes it convenient for request fan-out patterns.

Parallel.ForEachAsync is better when each item is processed independently and you manage any result collection yourself, typically with a thread-safe structure.

Both propagate failures, but the code shape is different. With Task.WhenAll, you tend to think in terms of task aggregation. With Parallel.ForEachAsync, you think in terms of processing one item at a time under a concurrency limit.

CPU Work Versus I/O Work

Many developers assume Parallel.ForEachAsync is only for CPU-bound work because of the word "Parallel." That is not true. It is also useful for async I/O when you want controlled fan-out, such as making API calls against a service that should not receive hundreds of concurrent requests.

Likewise, Task.WhenAll is not automatically wrong for I/O. It is often the cleanest choice for a handful of independent requests. The real problem appears only when the number of tasks explodes.

Common Pitfalls

  • Creating thousands of tasks and handing them to Task.WhenAll without any concurrency limit.
  • Using Parallel.ForEachAsync when you already have a small, fixed set of tasks that Task.WhenAll would express more clearly.
  • Ignoring MaxDegreeOfParallelism, which removes the main operational advantage of Parallel.ForEachAsync.
  • Assuming one API is always faster instead of looking at memory pressure, downstream load, and result-collection needs.
  • Forgetting cancellation handling for long-running or high-volume workloads.

Summary

  • 'Task.WhenAll waits for a set of tasks you already created.'
  • 'Parallel.ForEachAsync processes a sequence while controlling concurrency.'
  • Use Task.WhenAll for small or known task sets that should start together.
  • Use Parallel.ForEachAsync for larger collections where bounded fan-out matters.
  • Pick the API that matches the workload shape, not the one with the more impressive name.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.