ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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:

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.