Task over ValueTask<>
11/09/2025
I recently ran into some code that extensively used ValueTask types along with Task. I became curious if it is a good idea to mix async ValueTask<>s and Tasks. Moreover, .NET allows decorating methods returning ValueTask<> types with [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] that adds pooling behavior, which reduces allocations.
There are many ways to invoke async methods from another async method. The invoked async method can return a Task, ValueTask<>, or a pooled ValueTask<>; the calling method can also return any of these response types, and be a sync or async method. In this post, I create combinations of these methods to measure their performance footprint. The inner method may complete synchronously, asynchronously, or with a probability set between 0..1, where 1 means synchronous completion.
In this blog post, 'sync' completion refers to returning the result of an underlying async method without awaiting it, or without
.Result/.GetAwaiter().GetResult()calls. It does not refer to the 'sync-over-async' anti-pattern.
I used BenchmarkDotNet to measure the performance and allocations of these method combinations. Please note that the allocations show 'non-round' numbers due to BenchmarkDotNet's aggregation when the probability falls between 0 and 1 exclusively. Async completions invoke Task.Yield(); - which yields execution of the current task, allowing other tasks to execute. While there should be no other tasks running in the benchmark, the Mean performance results include a non-trivial waiting duration, that is for the task continuation to execute.