DEV Community

Adrián Bailador
Adrián Bailador

Posted on

Task vs ValueTask in C#

Asynchronous programming is a core part of modern .NET applications. But when it comes to performance-sensitive code, developers often face a difficult question:

Should I return Task or ValueTask?

At first glance, the difference appears trivial. In reality, the wrong choice can lead to unnecessary allocations, reduced throughput, and subtle bugs. This article provides a practical, real-world explanation, with clear rules to apply in your own applications.


1. What Problem Does ValueTask Solve?

Every time a method returns a Task, .NET allocates an object on the heap.

For most applications, this cost is negligible.

But in high-performance scenarios—where the same asynchronous method may be invoked millions of times—these allocations can accumulate and become expensive.

ValueTask<T> solves this by being a value type (struct), allowing .NET to avoid allocating a heap object when the result is already available synchronously.


2. Key Differences Between Task and ValueTask

Feature Task ValueTask
Type Reference type Value type
Heap allocation Always Avoided if completed synchronously
Awaitable multiple times Yes No
Safe to store and reuse Yes No
Supports pooling Yes No (but can wrap pooled Tasks)
Ideal scenarios General async code, I/O operations Hot paths, synchronous fast-paths
Complexity Low Higher

3. Example: A Good Use Case for ValueTask

Below is the classic example of a method that frequently returns synchronously from cache:

public ValueTask<string> GetItemAsync(string key)
{
    if (_cache.TryGetValue(key, out var value))
    {
        return new ValueTask<string>(value); // No allocation
    }

    return new ValueTask<string>(LoadFromDatabaseAsync(key));
}
Enter fullscreen mode Exit fullscreen mode

If LoadFromDatabaseAsync is only rarely called, ValueTask reduces the number of heap allocations dramatically.


4. A Bad Use Case for ValueTask

Use ValueTask incorrectly, and you may introduce subtle (and costly) bugs:

ValueTask<int> result = GetCachedValueAsync();

var a = await result;
var b = await result; // Not allowed — ValueTask may only be awaited once
Enter fullscreen mode Exit fullscreen mode

With Task, this is perfectly safe.


5. Performance Considerations

When ValueTask Saves Time

ValueTask shines when:

  • your method returns synchronously at least 80–90% of the time
  • the method is called very frequently (hundreds of thousands of calls or more)
  • you are inside a hot path such as parsers, protocol decoders, low-level libraries or high-throughput APIs

Removing millions of allocations per second can yield measurable improvements.

When ValueTask Makes Things Worse

Using ValueTask incorrectly can introduce:

  • additional copying (because it's a struct)
  • more IL instructions
  • added complexity for consumers
  • the risk of double-await bugs
  • unnecessary boxing in generic scenarios

For most applications, these costs outweigh any benefit.


6. Real-World Guidelines

Use Task by default

It is simpler, safer, and perfectly adequate for the vast majority of .NET applications.

Only use ValueTask when:

  • you have measured that Task allocation is a bottleneck
  • your method often completes synchronously
  • your code is part of a high-performance subsystem

Avoid ValueTask

  • in public APIs (unless you really need it)
  • when your method is rarely awaited synchronously
  • when consumers will store or re-await the result
  • when simplicity is more valuable than micro-optimisation

7. A Practical Example with Both Approaches

Using Task (recommended for most scenarios)

public Task<User> GetUserAsync(Guid id)
{
    return _database.LoadUserAsync(id);
}
Enter fullscreen mode Exit fullscreen mode

Clear, safe, conventional.

Using ValueTask (when data is often cached)

public ValueTask<User> GetUserAsync(Guid id)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return new ValueTask<User>(user);
    }

    return new ValueTask<User>(_database.LoadUserAsync(id));
}
Enter fullscreen mode Exit fullscreen mode

Avoids allocation in the synchronous path.


8. The Rule of 90%

A popular rule among .NET performance engineers:

Only use ValueTask if your method returns synchronously at least 90% of the time and you have benchmarks proving allocation is a bottleneck.

This rule alone prevents most incorrect uses of ValueTask.


9. Conclusion

ValueTask is a powerful tool for high-performance .NET applications, but it is often misunderstood and misused. For most developers and most APIs, Task remains the best choice:

  • simpler
  • safer
  • easier to reason about
  • suitable for almost all workloads

Use ValueTask only when performance measurements justify it, and when you understand its limitations.

You now have a clear, reliable framework for deciding between Task and ValueTask in your own applications.


Top comments (0)