C# ValueTask
last modified October 4, 2026
C# ValueTask tutorial shows how to work with the ValueTask and
ValueTask<TResult> types.
ValueTask and ValueTask<TResult> are struct
types that represent an asynchronous operation. Unlike Task, a
value task can wrap a result value directly, a Task, or an
IValueTaskSource. When an operation completes synchronously, a
value task stores the result inside the struct and does not allocate an object
on the heap. This makes it a good fit for methods that usually complete without
any asynchronous work, such as cache hits.
C# ValueTask example
In the first example, we create a value task that is already completed.
using System.Threading.Tasks; ValueTask<int> result = GetValue(); Console.WriteLine(result.IsCompletedSuccessfully); Console.WriteLine(result.Result); ValueTask<int> GetValue() => new ValueTask<int>(42);
The GetValue method returns a ValueTask<int>
that wraps the value 42 directly.
ValueTask<int> GetValue() => new ValueTask<int>(42);
This constructor stores the value inside the struct. No Task
object is created.
Console.WriteLine(result.IsCompletedSuccessfully);
The IsCompletedSuccessfully property tells whether the operation
has completed and whether it completed without an error.
Console.WriteLine(result.Result);
The Result property returns the wrapped value. Accessing it before
the operation completes blocks the current thread, so it should only be used
after the task has completed.
$ dotnet run True 42
C# ValueTask with async methods
An async method can return a ValueTask or a
ValueTask<TResult>. Such a method uses a value task builder
that reuses a single Task for many asynchronous completions and
avoids allocations when the method returns synchronously.
using System.Threading.Tasks;
Console.WriteLine(await GetGreetingAsync("Peter"));
Console.WriteLine(await GetGreetingAsync("Lucia"));
async ValueTask<string> GetGreetingAsync(string name)
{
await Task.Delay(100);
return $"Hello {name}";
}
We call the asynchronous GetGreetingAsync method twice and await
the returned value tasks.
async ValueTask<string> GetGreetingAsync(string name)
{
await Task.Delay(100);
return $"Hello {name}";
}
The method suspends on the Task.Delay call and resumes after the
delay. The compiler generates the asynchronous state machine for us.
$ dotnet run Hello Peter Hello Lucia
C# ValueTask is a value type
The difference between ValueTask and Task starts with
their kind. A Task is a reference type, while a
ValueTask is a value type.
using System.Threading.Tasks;
ValueTask<int> vt = new ValueTask<int>(42);
Task<int> task = Task.FromResult(43);
Console.WriteLine(typeof(ValueTask<int>).IsValueType);
Console.WriteLine(typeof(Task<int>).IsValueType);
Console.WriteLine($"{vt.Result} {task.Result}");
Both types represent the same kind of operation, but only Task is
an object on the heap.
$ dotnet run True False 42 43
C# ValueTask vs Task allocation
Because a value task is a struct, returning a completed value task does not
allocate memory. A task created with Task.FromResult is an object
that must be garbage collected. The following example measures the bytes
allocated by one hundred thousand calls of each kind.
using System.Threading.Tasks;
long taskBytes = MeasureTask();
long valueTaskBytes = MeasureValueTask();
Console.WriteLine($"Task: {taskBytes:N0} bytes");
Console.WriteLine($"ValueTask: {valueTaskBytes:N0} bytes");
long MeasureTask()
{
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100_000; i++)
{
_ = GetTask(i);
}
return GC.GetAllocatedBytesForCurrentThread() - before;
}
long MeasureValueTask()
{
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 100_000; i++)
{
_ = GetValueTask(i);
}
return GC.GetAllocatedBytesForCurrentThread() - before;
}
Task<int> GetTask(int x) => Task.FromResult(x + 1);
ValueTask<int> GetValueTask(int x) => new ValueTask<int>(x + 1);
The GC.GetAllocatedBytesForCurrentThread method reports how many
bytes have been allocated so far on the current thread.
long before = GC.GetAllocatedBytesForCurrentThread();
We record the number of allocated bytes before and after the loop, and return the difference.
$ dotnet run Task: 7,200,392 bytes ValueTask: 0 bytes
The Task version allocates a new object for each call. The
ValueTask version allocates nothing, because the values are stored
inside the structs.
C# ValueTask caching example
The typical use case for ValueTask is a method that often
completes synchronously. A cache lookup is a good example: on a cache hit, the
method returns the stored value without any asynchronous work.
using System.Threading.Tasks;
var cache = new Dictionary<string, decimal>
{
["apple"] = 1.2m,
["banana"] = 0.8m,
};
Console.WriteLine(await GetPriceAsync(cache, "apple"));
Console.WriteLine(await GetPriceAsync(cache, "banana"));
Console.WriteLine(await GetPriceAsync(cache, "cherry"));
async ValueTask<decimal> GetPriceAsync(Dictionary<string, decimal> cache,
string name)
{
if (cache.TryGetValue(name, out decimal price))
{
return price; // completes synchronously, without a Task allocation
}
// simulate an expensive lookup
await Task.Delay(100);
return 9.99m;
}
When the product is in the cache, the method returns immediately and no
Task is allocated. Only a cache miss goes through the asynchronous
path.
$ dotnet run 1.2 0.8 9.99
C# ValueTask.AsTask
A value task should be consumed only once. If you need to wait for the same
result more than once, or pass it to a method that expects a
Task, convert it with the AsTask method.
using System.Threading.Tasks; ValueTask<int> vt = GetValue(); Task<int> task = vt.AsTask(); Console.WriteLine(await task); Console.WriteLine(await task); ValueTask<int> GetValue() => new ValueTask<int>(42);
The AsTask call returns a Task<int> that can be
awaited or combined with other tasks as many times as necessary.
$ dotnet run 42 42
C# ValueTask and IValueTaskSource
Besides a value and a Task, a value task can be backed by an
IValueTaskSource<TResult>. This interface allows a single
object to serve many operations, so a pool of reusable objects can further
reduce allocations in high-throughput code. The
ManualResetValueTaskSourceCore<TResult> type implements most
of the logic needed for such a source.
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
var source = new DelaySource();
Console.WriteLine(await source.GetValueAsync(100));
Console.WriteLine(await source.GetValueAsync(200));
sealed class DelaySource : IValueTaskSource<int>
{
private ManualResetValueTaskSourceCore<int> _core;
public ValueTask<int> GetValueAsync(int delay)
{
_core.Reset();
_ = CompleteAsync(delay);
return new ValueTask<int>(this, _core.Version);
}
private async Task CompleteAsync(int delay)
{
await Task.Delay(delay);
_core.SetResult(delay);
}
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);
}
The source resets the core before each operation and passes the current version as the token. The token lets the source detect a stale consumer that tries to use a previous operation.
$ dotnet run 100 200
C# ValueTask guidelines
A ValueTask is a struct, so it is copied when passed around. This
imposes rules that do not apply to Task:
- Await a value task only once. Do not await it again after
AsTaskhas been called. - Do not read
Resultor callGetAwaiter().GetResultbefore the operation completes. - Do not use a value task with combinators such as
Task.WhenAllorTask.WhenAny; convert it withAsTaskfirst. - Do not reuse a value task in a loop that may consume the same instance twice.
Prefer Task and Task<TResult> for general
asynchronous code. Use ValueTask when a method is expected to
complete synchronously most of the time and profiling shows that the
allocations matter. Changing a public method from Task to
ValueTask is a breaking change for callers, so choose carefully.
Source
ValueTask<TResult> Struct - Microsoft Learn
Understanding the Whys, Whats, and Whens of ValueTask
In this article we have worked with the ValueTask type in C#.
Author
List all C# tutorials.