Task
Task was introduced in .NET Framework 4.0 as part of the Task Parallel Library (TPL) and later became the foundation of async/await in .NET Framework 4.5. It represents an asynchronous operation that may complete in the future and integrates seamlessly with exception handling, cancellation, and composition APIs like Task.WhenAll. Because it is simple, reliable, and heavily optimized by the runtime, Task is the recommended return type for the vast majority of asynchronous methods in both application and library code.
ValueTask
ValueTask was introduced in .NET Core 2.0 (and later made available in .NET Standard 2.1) as a lightweight alternative to Task for high-performance scenarios. It is designed to reduce heap allocations when an operation frequently completes synchronously, allowing a result to be returned without creating a Task object. However, it introduces additional complexity and usage constraints, so it should only be used in performance-critical paths where measurements demonstrate a clear benefit.
So, with that in mind, is it better or should we always use ValueTask instead of Task?
Let's check it out together.
real-world scenarios where you should NOT use ValueTask
1- If the operation completes asynchronously most of the time → use Task
❌ Bad use of ValueTask
public async ValueTask<string> GetUserFromDatabaseAsync(int id)
{
return await _db.Users.FindAsync(id);
}
Why this is wrong
- Database calls are almost always asynchronous
- The operation will almost never complete synchronously
- So ValueTask will just wrap a Task internally
- You gain nothing
- You add complexity
✅ Correct version
public async Task<string> GetUserFromDatabaseAsync(int id)
{
return await _db.Users.FindAsync(id);
}
2- When your method is part of public APIs → Use Task.
If you're writing:
- Application services
- Controllers
- Business logic
- Library APIs consumed by others
Because users may:
var task = service.GetAsync();
await Task.WhenAll(task, anotherTask);
Task.WhenAll does NOT accept ValueTask.
Now they must do:
await Task.WhenAll(task.AsTask(), anotherTask);
That creates allocations anyway and defeating the purpose.
3- When the method is rarely called
If this method runs:
- 10 times per request
- 1000 times per day
You absolutely do not need ValueTask.
Task allocation cost is tiny compared to:
- DB calls
- HTTP calls
- JSON parsing
- Logging
- GC pressure from real objects
4- When the method contains await and does real async work → prefer Task
This is subtle but important.
If your method is:
public async ValueTask<int> CalculateAsync()
{
await Task.Delay(10);
return 5;
}
The compiler builds an async state machine.
Now ValueTask gives you:
- Larger state machine
- More complex generated code
- No benefit
In fact, ValueTask async methods are often slightly slower.
5- When the result is almost never synchronous
Good case for ValueTask:
public ValueTask<int> GetCachedValueAsync()
{
if (_cache.TryGetValue(out var value))
return new ValueTask<int>(value);
return new ValueTask<int>(SlowOperationAsync());
}
Bad case:
public ValueTask<int> AlwaysSlowAsync()
{
return new ValueTask<int>(SlowOperationAsync());
}
If you're just wrapping a Task, you're wasting everyone's time.
6- When used in LINQ / functional composition
This is where people get burned.
var results = await Task.WhenAll( items.Select(x => service.GetAsync(x)));
If GetAsync returns ValueTask, this breaks.
You must convert each one:
items.Select(x => service.GetAsync(x).AsTask())
Now you’re allocating anyway.
7- When storing for later
This is unsafe/ dangerous:
private ValueTask<int> _storedTask;
public void Save()
{
_storedTask = GetAsync(); // ❌ risky
}
ValueTask is not meant to be stored long-term.
8- await it multiple times, use Task
ValueTask<int> vt = GetAsync();
await vt;
await vt; // ❌ dangerous
If you need multiple awaits:
await vt.AsTask();
When you’re not writing high-performance infrastructure
Real-world places where ValueTask makes sense:
- ASP.NET Core internals
- System.IO pipelines
- Socket implementations
- Kestrel server
- High-frequency parsers
Not:
- CRUD apps
- Microservices
- Background jobs
- Web APIs
- Normal backend code
ValueTask limitation
- ❌ Can only be awaited once (unless converted to Task)
- ❌ Cannot be reused
- ❌ More complex semantics
- ❌ Bigger struct (copies cost more)
- ❌ Easy to misuse
- ❌ Breaks common patterns like caching tasks
👉 Use Task unless you have measured a real performance issue caused by allocations
If you’ve ever debated Task vs ValueTask in a code review — I’d love to hear your thoughts in the comments.