Using lambda expression instead of asynchronous version of Write method
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When performing I/O operations in C#, developers sometimes wrap a synchronous Write call inside a lambda and pass it to Task.Run, believing this achieves the same result as calling the built-in WriteAsync method. While both approaches free the calling thread, they work in fundamentally different ways under the hood, and choosing the wrong one can waste thread pool resources and hurt scalability. Understanding this distinction is critical for writing efficient asynchronous code in .NET.
Why Async I/O APIs Exist
Operating systems provide asynchronous I/O primitives that allow a write request to be submitted and completed later without blocking any thread. The .NET Stream.WriteAsync method leverages these OS-level primitives. When you call WriteAsync, the runtime submits the I/O request and returns a Task immediately. No thread sits idle waiting for the disk or network to finish. The thread pool thread is free to handle other work.
This is why the framework provides dedicated async methods rather than expecting developers to wrap synchronous calls manually. True async I/O scales far better because it does not consume a thread during the wait.
The Lambda Wrapper Approach
A common pattern developers reach for is wrapping the synchronous Write method in a Task.Run lambda:
This does move the blocking call off the calling thread, which can be useful in a UI application to keep the interface responsive. However, it still blocks a thread pool thread for the entire duration of the I/O operation. Under heavy load, this approach exhausts the thread pool and creates a bottleneck.
The True Async Approach
The correct approach for I/O-bound work is to use the async API directly:
Notice the useAsync: true parameter in the FileStream constructor. This tells .NET to use overlapped I/O on Windows, enabling genuine asynchronous operations. Without this flag, even WriteAsync may fall back to blocking a thread pool thread internally.
Side-by-Side Comparison
Here is a practical example showing both approaches in a method that writes data to a file:
Both methods return a Task and can be awaited. The difference is invisible to the caller but significant at scale. WriteSyncWrappedAsync ties up a thread pool thread for the entire write, while WriteTrueAsync releases it during the actual I/O wait.
When Each Approach Is Appropriate
The lambda-with-Task.Run pattern is not always wrong. It is appropriate in specific situations:
Use Task.Run with a lambda when:
- You are in a UI application (WPF, WinForms) and need to move CPU-bound or legacy synchronous work off the UI thread.
- You are calling a third-party library that provides no async API and you cannot change it.
Use the native async API when:
- You are writing server-side code (ASP.NET, background services) where thread pool efficiency matters.
- The framework provides an async version of the method (which is the case for all modern .NET I/O APIs).
- You need to handle high concurrency without exhausting threads.
Common Pitfalls
- Using
Task.Runfor I/O in ASP.NET: This wastes a thread pool thread and adds overhead from the thread switch without any benefit, since there is no UI thread to unblock. - Forgetting
useAsync: trueonFileStream: Without this flag,WriteAsyncinternally blocks a thread pool thread, negating the benefit of the async call. - Mixing sync and async in the same stream: Calling both
WriteandWriteAsyncon the same stream can cause race conditions and corrupted output. - Not awaiting the returned task: Forgetting
awaitonWriteAsyncmeans the write may not complete before the stream is disposed. - Wrapping
WriteAsyncinsideTask.Run: This double-wraps the operation, wasting a thread pool thread to await an already-async operation.
Summary
Stream.WriteAsyncuses OS-level async I/O and releases the thread during the wait, making it the right choice for server-side and high-concurrency scenarios.- Wrapping synchronous
WriteinTask.Run(() => ...)still blocks a thread pool thread and should only be used to offload work from a UI thread or when no async API exists. - Always pass
useAsync: truewhen constructing aFileStreamto ensureWriteAsyncuses true asynchronous I/O. - On ASP.NET and other server-side frameworks, prefer native async APIs over
Task.Runwrappers to maintain thread pool health under load.

