🚀 C# Async Tip — Task.WhenEach vs Task.WhenAll vs Serialized await
These three patterns look similar, but they solve different problems:
WhenAll→ wait for everything, then continueWhenEach→ process results as tasks complete- Serialized
await→ run one at a time (or control concurrency)
1️⃣ Task.WhenAll — “I need all results”
Best when you must wait for everything (e.g., build a combined response).
var tasks = ids.Select(id => client.GetAsync(id)).ToArray(); var results = await Task.WhenAll(tasks); // completes when ALL complete return results;
✅ Fast overall (parallel)
✅ Great for “fan-out / fan-in”
⚠️ If one fails, the returned task faults (you’ll handle exceptions at the await).
Use when: you only care after everything is done.
2️⃣ Task.WhenEach — “I want results as soon as they’re ready”
WhenEach returns an IAsyncEnumerable<Task> that yields tasks as they complete, so you can stream/handle progress naturally.
var tasks = urls.Select(url => http.GetStringAsync(url)).ToArray(); await foreach (var completed in Task.WhenEach(tasks)) { try { var body = await completed; // observe result (or exception) Console.WriteLine(body.Length); // process immediately } catch (Exception ex) { Console.WriteLine(ex.Message); // handle per-task failures } }
✅ Best UX/throughput for “process as you go”
✅ Great for pipelines, progress updates, partial success
⚠️ You still need to await each yielded task to observe exceptions/results
Use when: you benefit from early results (UI updates, streaming, incremental aggregation).
3️⃣ Serialized await — “I need it sequential (or controlled)”
True serialized execution = start the next operation only after the previous finishes:
foreach (var url in urls) { var body = await http.GetStringAsync(url); // one-at-a-time Process(body); }
✅ Predictable load (good for rate limits)
✅ Simplest debugging
❌ Slowest wall-clock time
Use when: the dependency is sequential, or you must respect strict rate limits / ordering.
⚠️ Important gotcha: “Serialized await” can be fake
This starts everything in parallel, then awaits one by one (still parallel execution!):
var tasks = urls.Select(http.GetStringAsync).ToList(); foreach (var t in tasks) { var body = await t; // tasks already running Process(body); }
If you want true control, you need sequential awaits or a concurrency limiter.
🧠 Quick rule of thumb
- Need all results at the end? →
Task.WhenAll - Want to process as soon as each finishes? →
Task.WhenEach - Need strict ordering / rate limiting / less load? → Serialized
await
🎯 Final takeaway
This isn’t a “which is faster” debate. It’s a workflow decision:
WhenAlloptimizes for completionWhenEachoptimizes for throughput + responsiveness- Serialized
awaitoptimizes for control
#dotnet #csharp #async #performance #softwareengineering