Every async method that returns Task<T> allocates a Task<T> object on the heap — even when the method completes synchronously. For methods that frequently complete without awaiting (cache hits, buffered reads, short-circuit logic), ValueTask<T> can eliminate this allocation entirely.

The Problem with Task<T>

Consider a caching layer:

Example.cs
public async Task<User> GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out var user))
        return user; // synchronous path — but still allocates a Task<User>

    user = await _database.GetUserAsync(id);
    _cache[id] = user;
    return user;
}

When the cache hits (the common case), this method completes synchronously. But Task<User> is a reference type — returning one always allocates on the heap. Under high throughput, these allocations create measurable GC pressure.

ValueTask<T> to the Rescue

ValueTask<T> is a struct that wraps either a T value (for synchronous completion) or a Task<T> (for asynchronous completion):

Example.cs
public ValueTask<User> GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out var user))
        return new ValueTask<User>(user); // zero allocation

    return GetUserSlowAsync(id);
}

private async ValueTask<User> GetUserSlowAsync(int id)
{
    var user = await _database.GetUserAsync(id);
    _cache[id] = user;
    return user;
}

On the synchronous path, ValueTask<User> wraps the result directly — no heap allocation. On the asynchronous path, it wraps a Task<User> internally, so there is no additional overhead compared to returning Task<User>.

When ValueTask Helps

ValueTask<T> is beneficial when:

When to Stick with Task

Task<T> is safer and more flexible. Prefer it unless you have a specific reason to use ValueTask<T>:

Example.cs
// SAFE — Task can be awaited multiple times
Task<int> task = GetValueAsync();
int a = await task;
int b = await task; // fine

// DANGEROUS — ValueTask must only be awaited once
ValueTask<int> vt = GetValueAsync();
int a = await vt;
int b = await vt; // UNDEFINED BEHAVIOUR

The Rules of ValueTask

The .NET documentation is explicit about ValueTask<T> constraints:

  1. Never await a ValueTask more than once.
  2. Never use GetAwaiter().GetResult() before the ValueTask completes.
  3. Never use .Result on an incomplete ValueTask.
  4. If you need to do any of these things, call .AsTask() first.
Example.cs
// If you need Task-like flexibility, convert immediately
ValueTask<int> vt = GetValueAsync();
Task<int> task = vt.AsTask(); // now it's a regular Task

IValueTaskSource: Advanced Pooling

For maximum performance, you can implement IValueTaskSource<T> to pool the underlying state machine objects. This is what System.IO.Pipelines does internally:

PooledOperation.cs
public class PooledOperation : IValueTaskSource<int>
{
    private ManualResetValueTaskSourceCore<int> _core;

    public int GetResult(short token) => _core.GetResult(token);

    public ValueTaskSourceStatus GetStatus(short token) =>
        _core.GetStatus(token);

    public void OnCompleted(
        Action<object?> continuation,
        object? state,
        short token,
        ValueTaskSourceOnCompletedFlags flags) =>
        _core.OnCompleted(continuation, state, token, flags);

    public void SetResult(int result) => _core.SetResult(result);

    public void Reset() => _core.Reset();

    public ValueTask<int> AsValueTask() =>
        new(this, _core.Version);
}

This is advanced territory — only Socket, Pipe, and similar high-throughput infrastructure typically need this.

Benchmark Comparison

TaskVsValueTask.cs
[MemoryDiagnoser]
public class TaskVsValueTask
{
    [Benchmark]
    public async Task<int> WithTask() => 42;

    [Benchmark]
    public async ValueTask<int> WithValueTask() => 42;

    [Benchmark]
    public ValueTask<int> WithValueTaskSync() => new(42);
}

Typical results:

Method Mean Allocated
WithTask ~25 ns 88 B
WithValueTask ~25 ns 88 B
WithValueTaskSync ~0 ns 0 B

The key insight: async ValueTask<T> still allocates a state machine when it actually awaits. The win comes only from the synchronous path where you return new ValueTask<T>(result) directly.

Summary

Use ValueTask<T> when a method frequently completes synchronously and is called at high frequency. Use Task<T> everywhere else. Never await a ValueTask more than once. And always benchmark — the allocation savings from ValueTask<T> are only meaningful in genuinely hot paths.