ValueTask is not a better Task. It is a sharper tool for specific async hot paths.
Use Task or Task<T> by default. They are familiar, easy to compose, safe to store, safe to pass around, and work naturally with APIs like Task.WhenAll. Most application code benefits more from that simplicity than from avoiding task allocation on a path that is not actually hot. ValueTask also has overhead of its own because it is a larger struct, so it is not an automatic performance win.
Reach for ValueTask<T> when the method often completes synchronously and the allocation matters, or when lower-level library code has a deliberate reusable async source strategy. Common examples are cache lookups, pooled readers, parsers, protocol code, or library APIs that sit inside tight loops. Even then, measure first.
public ValueTask<Customer?> GetCustomerWithCacheAsync(
string customerId,
CancellationToken cancellationToken)
{
if (_cache.TryGetValue(customerId, out Customer? cached))
{
return ValueTask.FromResult<Customer?>(cached);
}
return new ValueTask<Customer?>(
LoadCustomerAsync(customerId, cancellationToken));
}
The shape only makes sense because the synchronous path is common and cheap. If the asynchronous path is the common path, this usually just wraps a Task and shifts complexity to the caller. The same caution applies to non-generic ValueTask: for async methods without a result, Task should still be the default unless performance analysis says otherwise.
That contract matters. A ValueTask returned from an arbitrary method should normally be awaited once and directly, or converted once with AsTask() if you need task-style composition:
Customer? customer =
await repository.GetCustomerWithCacheAsync(customerId, cancellationToken);
Do not store it for later, await it twice, block on it, or feed it into APIs that expect reusable tasks. Be especially careful with ValueTask in public APIs, because the optimization leaks a stricter consumption contract to every caller.
Use ValueTask when you can explain the synchronous completion path, the allocation you are avoiding, and the caller behavior you expect. If the explanation is “it sounds faster”, use Task.