C#  

Why ValueTask Can Save You from Async Overhead

Asynchronous programming in C# is built on top of the Task and async/await model. For most scenarios, using Task is perfectly fine. But if you’re writing high-performance code , you’ve probably heard about ValueTask.

So, when should you use ValueTask instead of Task? Let’s break it down.

This is highly related to memory allocation, so let's always try to see it from this perspective.

What is a Task?

Tasks represent a promise of an operation that will eventually be completed. It encapsulates the operation that can be executed independently on a different thread from the one that initialized it.

Let’s understand how tasks work with allocation.

Case 1. Task Synchronous Completion

If your method completes synchronously (returns immediately without awaiting anything), the compiler/runtime still has to return a Task object.

(Usually, it uses a cached instance like Task.FromResult(value) )

Note: This still creates or reuses a heap-allocated Task<T> object.

  
    public Task<int> GetNumberTask(bool cached)
{
    if (cached)
        return Task.FromResult(42); // allocation (cached instance but still a heap object)
    return DoRealWorkAsync();
}
  

So, even if the value is already known (42), you pay the allocation cost for a Task<int> object. Even though int is a value type.

And even if the result is already known immediately , the runtime must return a Task<T> instance.

Case 2. Task Asynchronous Completion

When the method awaits something:

  1. The compiler generates an async struct that tracks the execution.

  2. It is boxed and tied to an object to represent the ongoing work.

  3. A new heap-allocated Task<T> is created for the async result.

          
            public async Task<int> GetNumberTaskAsync()
    {
        await Task.Delay(100);  // async suspension point
        return 42;
    }
          
        

=> Async completion always involves allocations. synchronous may not.

What is ValueTask?

This is where ValueTask<T> saves the day. Introduced in .NET Core 2.0 to reduce allocation overhead. A struct is designed to optimize asynchronous operations.

Unlike Tasks, Value tasks are a value type, meaning we can avoid heap allocation when the operation completes synchronously

Case 1. Synchronous Completion

  • If you already know the result, the value is stored directly in the ValueTask<T> struct.

  • No heap allocation occurs.

  
    public ValueTask<int> GetNumberValueTaskSync()
{
    return new ValueTask<int>(42); // Value stored inline, no heap
}
  

Case 2. Asynchronous Completion:

If the method actually awaits something:

  • ValueTask<T> internally wraps a Task<T>.

  • That means you still get heap allocations, just like with Task<T>.

  
    public async ValueTask<int> GetNumberValueTaskAsync()
{
    await Task.Delay(100);   // async suspension point
    return 42;
}
  

Why does this really matter as a .NET developer?

Understanding the difference between Task and ValueTask is crucial when writing high-performance asynchronous code in C#.

  • Task<T> is simple and reliable, but even for synchronous completions, it may incur heap allocations. This overhead is usually negligible, but in tight loops or performance-critical paths, it can add up.

  • ValueTask<T> shines in scenarios where the result is often already available synchronously. Since it’s a struct, it can store the result inline and avoid heap allocations, reducing GC pressure.

Here, the benchmarking results can show clearly the difference between using ValueTask and a task in Synchronous completion.

method

However, when an operation truly requires asynchronous execution, ValueTask<T> internally wraps a Task<T> anyway, so the allocation difference disappears.

By carefully choosing between Task and ValueTask, you can write cleaner, faster, and more memory-efficient async code, especially in performance-critical applications.