Using async to run multiple methods
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Using async does not automatically make multiple methods run at the same time. In C#, async mostly changes how you wait for work. If you want several independent asynchronous operations to overlap, you need to start them first and then await their completion together.
The standard pattern is Task.WhenAll. It is simple, but the details matter: sequential await is still sequential, exceptions are aggregated differently, and CPU-bound work is a separate problem from I/O-bound async calls.
Start Tasks Before Awaiting Them
This code looks asynchronous, but from the caller's point of view it still runs one step after another:
Each method starts only after the previous one has completed. To allow the operations to overlap, create the tasks first:
This works well when the methods are independent and mostly waiting on I/O such as HTTP calls, database queries, or file access.
A Runnable Example with Task.WhenAll
The pattern is clearer in a small console example:
The total runtime is close to the longest delay rather than the sum of all delays because the waits overlap.
Limit Concurrency When Needed
Sometimes you do not want to start every operation at once. If you are making network calls to an external service or reading many files, unbounded concurrency can create throttling or memory pressure. A SemaphoreSlim lets you cap the number of simultaneous operations.
This keeps the non-blocking behavior while protecting the system from excessive fan-out.
Distinguish Async I/O from CPU Parallelism
async is best for operations that spend time waiting. It does not automatically spread CPU-heavy work across multiple cores. If the workload is CPU-bound, Task.Run or another parallelism tool may be appropriate.
That is a different decision from normal async I/O. Use it deliberately. Wrapping everything in Task.Run is not good async design.
Common Pitfalls
The biggest mistake is awaiting each async method immediately and expecting overlap. If you await one call before starting the next, the caller still experiences sequential execution.
Another issue is using Task.WhenAll when operations are not really independent. If method B needs the result of method A, forcing concurrency only makes the code harder to reason about.
Error handling also changes. If one task fails inside Task.WhenAll, the combined await fails as well. That may be exactly what you want, but you should decide that consciously.
Finally, do not confuse non-blocking I/O with CPU parallelism. They solve related but different performance problems.
Summary
- Start independent tasks first, then await them together.
- Use
Task.WhenAllto overlap asynchronous operations cleanly. - Apply concurrency limits when external services or memory usage matter.
- Use
Task.Runonly for intentional CPU-bound offloading. - Keep the code sequential when the operations depend on each other.

