Building .NET apps requires an async-first approach. The application patterns that maximise its value have been established through production experience over the past ten years.
Async matters for throughput because ASP.NET Core's threading model relies on the thread pool. When a request thread blocks waiting for I/O, such as a database query, HTTP call, or file read, it can't serve other requests. In synchronous applications, each concurrent request needs a thread. In async applications, one thread can handle many concurrent I/O-bound requests by releasing 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.
In the last couple of years I ran a payment gateway that peaked at 12 k requests per second. The synchronous version choked around 2 k RPS before the thread pool exhausted and latency spiked past 2 seconds. Switching the DB and HTTP calls to true async cut the thread count from ~150 to under 30 and the same hardware sustained the 12 k load with sub‑100 ms latency. I had to bump the min thread count to 100 to avoid the warm‑up dip, but the real win was the reduction in context switches and the lower GC pressure.
The async all the way principle ensures async works best when consistent throughout the call chain. A single synchronous blocking call in an otherwise async chain degrades throughput to synchronous levels for that operation. A common violation is calling .Result or .Wait() on a Task in an otherwise async code path. This can also cause deadlocks in synchronisation-context environments. The rule is simple: if the callee is async, the caller should be async.
When choosing between ValueTask and Task, consider that 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, such as cache hits or in-memory lookups, using ValueTask reduces GC pressure. The guideline is to use Task for most cases and ValueTask for hot paths where allocation measurement shows it matters.
ValueTask looks tempting, but I learned the hard way that exposing it from a public API can bite you when callers combine it with .ConfigureAwait(false) and then call .Result later. The extra state machine can also double‑allocate if you await it more than once. In a micro‑service that hit the cache 95 % of the time we wrapped the cache lookup in a ValueTask, measured a 12 % drop in Gen 2 collections, and kept the method internal. Once we opened it up, the extra allocations erased the benefit, so the rule of thumb became: keep ValueTask behind a thin, well‑tested layer.
For producer-consumer patterns, .NET Core 3.0 introduced System.Threading.Channels.Channel, which provides a thread-safe, async-friendly producer-consumer queue. This 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, while BlockingCollection and ConcurrentQueue are synchronous alternatives for the same use case.
Channels are great until you forget about back‑pressure. In a log‑ingestion pipeline we set BoundedChannelOptions.Capacity to 5 000 and used SingleWriter = true because the HTTP endpoint was the only producer. When the downstream writer fell behind during a spike, the channel started rejecting writes and we surfaced a 429 to the client. Adding a small retry loop and exposing the channel's Completion token to the host allowed the service to drain gracefully on shutdown instead of losing messages. The trade‑off is a bit more code, but it saved us from a silent data loss that would have taken weeks to debug.
Async-first architecture is no longer optional for .NET applications. It's essential to understand the benefits and best practices to maximise its value.
The benefits of async-first architecture are clear. By adopting async patterns and using the right tools, .NET developers can build high-performance applications that handle high concurrency with ease.
In modern .NET applications, async-first architecture is crucial for achieving high throughput and scalability. By following best practices and using tools like Channel, developers can build efficient and scalable applications.