0Pricing
C# Academy · Lesson

ValueTask & Avoiding Allocations

Use ValueTask to reduce heap allocations in hot paths, understand its constraints, and avoid common misuse pitfalls.

ValueTask & Avoiding Allocations is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Allocation Problem with Task

Task<T> is a reference type — every async method allocates a Task on the heap, even if the result is available synchronously (e.g., from a cache). In hot paths this can cause significant GC pressure. ValueTask<T> solves this.

What Is ValueTask?

ValueTask<T> is a struct that can represent either a synchronously available result or a pending async operation. When the result is synchronous, no heap allocation is needed.

// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
    _cache.TryGetValue("key", out var v) ? v : await FetchAsync();

// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
    _cache.TryGetValue("key", out var v)
        ? ValueTask.FromResult(v)  // no allocation
        : new ValueTask<string>(FetchAsync()); // allocation only when needed

Returning ValueTask from an Interface

Use ValueTask<T> in interface methods that are frequently called and often complete synchronously. Consumers use it exactly like Task<T>.

public interface IProductRepository
{
    // ValueTask: often completes synchronously from cache
    ValueTask<Product?> GetByIdAsync(int id);

    // Task: always async (DB query)
    Task<List<Product>> SearchAsync(string query);
}

// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");

ValueTask with IValueTaskSource

For extreme perf, implement IValueTaskSource<T> to pool the backing state object. This avoids all allocations even for async completions — used internally by .NET's socket and I/O code.

// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.

// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughput

ValueTask Constraints: Await Only Once

Critical rule: a ValueTask<T> must be awaited at most once, and you cannot call .Result before it completes. Violating this causes undefined behavior.

var vt = repo.GetByIdAsync(42);

// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!

// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG

// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple times

ValueTask<T> vs Task<T> Decision Guide

Use ValueTask<T> only when profiling shows allocation pressure from async methods. Don't use it everywhere — it adds complexity without benefit in low-frequency code.

// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O

// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share it

Non-Generic ValueTask

ValueTask (non-generic) works for methods returning Task (void async). Use it for frequently called methods that often complete synchronously.

public interface ICache
{
    // Often a no-op when value exists — ValueTask avoids allocation
    ValueTask SetAsync(string key, string value);
    ValueTask InvalidateAsync(string key);
}

public class InMemoryCache : ICache
{
    private readonly Dictionary<string, string> _data = new();

    public ValueTask SetAsync(string key, string value)
    {
        _data[key] = value;
        return ValueTask.CompletedTask; // no allocation
    }
}

Measuring Allocation with BenchmarkDotNet

Use [MemoryDiagnoser] in BenchmarkDotNet to measure allocations per operation. This is the only reliable way to verify that ValueTask is actually helping.

[MemoryDiagnoser]
public class CacheBenchmark
{
    private readonly CachedRepo _repo = new();

    [Benchmark]
    public async Task<Product?> Task_GetById()
        => await _repo.GetByIdTask(1);

    [Benchmark]
    public async ValueTask<Product?> ValueTask_GetById()
        => await _repo.GetByIdValueTask(1);
}

// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hit

ValueTask in SocketsHttpHandler

The .NET HTTP stack uses ValueTask extensively internally. Understanding this helps you write high-performance HTTP clients that align with the runtime's allocation strategy.

// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/O

Real-World: Hot Path Cache Service

A cache service where every call is potentially a cache hit — ValueTask eliminates allocations on the hot path while still supporting async fallback for cache misses.

public class ProductCache
{
    private readonly Dictionary<int, Product> _cache = new();
    private readonly IProductRepository _repo;

    public ProductCache(IProductRepository repo) => _repo = repo;

    public ValueTask<Product?> GetAsync(int id)
    {
        // Cache hit: no allocation
        if (_cache.TryGetValue(id, out var cached))
            return ValueTask.FromResult<Product?>(cached);

        // Cache miss: async fetch
        return new ValueTask<Product?>(FetchAndCacheAsync(id));
    }

    private async Task<Product?> FetchAndCacheAsync(int id)
    {
        var product = await _repo.GetByIdAsync(id);
        if (product is not null) _cache[id] = product;
        return product;
    }
}

Quick Check

What is the critical rule about awaiting a ValueTask?

Recap: ValueTask & Avoiding Allocations

Key takeaways:

  • ValueTask<T>: struct that avoids heap allocation when result is synchronous
  • Best for hot paths that frequently complete synchronously (cache hits)
  • NEVER await a ValueTask<T> more than once — use .AsTask() to share
  • Use [MemoryDiagnoser] to verify allocation savings before optimizing
  • Prefer Task<T> for general use; use ValueTask<T> only when profiling justifies it

Frequently asked questions

Is the “ValueTask & Avoiding Allocations” lesson free?

Yes — the full text of “ValueTask & Avoiding Allocations” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “ValueTask & Avoiding Allocations”?

Use ValueTask to reduce heap allocations in hot paths, understand its constraints, and avoid common misuse pitfalls. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “ValueTask & Avoiding Allocations” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. IAsyncEnumerable & await foreach
  2. System.Threading.Channels
  3. ValueTask & Avoiding Allocations
  4. ConfigureAwait & Synchronization Context
← Back to C# Academy