If you're building .NET apps, async-first isn't optional anymore. Ten years later, the application patterns that maximise its value have been established through production experience.

Why async matters for throughput

ASP.NET Core's threading model is based on the thread pool. When a request thread blocks waiting for I/O (database query, HTTP call, file read), that thread cannot serve other requests. In a synchronous application, each concurrent request requires a thread. In an async application, one thread can handle many concurrent I/O-bound requests because it releases back to the thread pool while waiting. Under high concurrency with I/O-bound workloads, async ASP.NET Core can handle 5-10x more concurrent requests with the same number of threads.

The async all the way principle

Async works best when it is consistent throughout the call chain. A single synchronous blocking call in an otherwise async chain degrades throughput to synchronous levels for that operation. The common violation is calling .Result or .Wait() on a Task in a code path that is otherwise async. This can also cause deadlocks in synchronisation-context environments. The rule is: if the callee is async, the caller should be async.

ValueTask vs Task

Task allocates an object on the heap for every call, even when the result is available synchronously. ValueTask is a value type that avoids this allocation when the result is already available. For high-frequency code paths where the result is often available without I/O (cache hits, in-memory lookups), using ValueTask reduces GC pressure. The guideline: use Task for most cases, use ValueTask for hot paths where allocation measurement shows it matters.

Channel for producer-consumer patterns

System.Threading.Channels.Channel, introduced in .NET Core 3.0, provides a thread-safe, async-friendly producer-consumer queue. The pattern is useful for: offloading work from HTTP request handlers to background processing, decoupling ingestion from processing in event-driven systems, and implementing bounded concurrency in parallel processing pipelines. Channel is the right tool for these patterns; BlockingCollection and ConcurrentQueue are the synchronous alternatives for the same use case.