use ValueTask<T> when your async method often returns a result synchronously — it avoids unnecessary heap allocations and is faster under load.
the problem with Task<T>
every Task<T> is a heap-allocated object. for methods that resolve synchronously most of the time (e.g., cache hits), this creates a lot of garbage for no real benefit.
the fix with ValueTask<T>
ValueTask<T> is a struct. when the result is already available, it returns inline — zero allocation.
internal interface ILookupService
{
ValueTask<IList<LookupModel>> GetCategoriesAsync();
}
when to use it
| Scenario | Use |
|---|---|
| Result often comes from cache (sync) | ValueTask<T> |
| Always awaits I/O (DB, HTTP) | Task<T> |
| Called in loop that service mostly returns sync results | ValueTask<T> |
don't use
ValueTask<T>everywhere — only where performance profiling shows benefit