C#
.NET
concurrency
BlockingCollection
performance optimization

Channel/BlockingCollection alloc free alternatives?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

High-throughput pipelines in .NET often suffer from garbage collection pauses when each message allocates temporary objects. Many teams ask for an "allocation-free queue" as a silver bullet, but the queue is only one part of memory pressure. The effective strategy is combining modern queue primitives with pooled buffers and predictable message shapes.

Measure Before Replacing Primitives

BlockingCollection can be good enough in moderate workloads, and replacing it without profiling can waste time. First identify where allocations happen in your hot path.

A quick runtime check:

bash
dotnet-counters monitor --process-id 12345 System.Runtime

Watch allocation rate and GC counts while running representative load. If allocations remain high after queue replacement, root cause is likely payload creation, serialization, logging, or per-item task creation.

Prefer Bounded Channel for Async Pipelines

For modern async producer and consumer workloads, System.Threading.Channels usually gives better ergonomics and backpressure behavior than BlockingCollection.

csharp
1using System.Threading.Channels;
2
3var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(2048)
4{
5    SingleWriter = false,
6    SingleReader = true,
7    FullMode = BoundedChannelFullMode.Wait
8});

Bounded capacity avoids unbounded heap growth during spikes. FullMode.Wait slows producers instead of dropping work.

Producer and consumer example:

csharp
1var producer = Task.Run(async () =>
2{
3    for (int i = 0; i < 100_000; i++)
4    {
5        await channel.Writer.WriteAsync(i);
6    }
7    channel.Writer.Complete();
8});
9
10var consumer = Task.Run(async () =>
11{
12    await foreach (var item in channel.Reader.ReadAllAsync())
13    {
14        _ = item * 2;
15    }
16});
17
18await Task.WhenAll(producer, consumer);

Reduce Payload Allocations with ArrayPool

If each message carries bytes, pooling can eliminate repeated large allocations.

csharp
1using System.Buffers;
2
3byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
4try
5{
6    // fill and process buffer
7}
8finally
9{
10    ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
11}

The ownership rule is strict: one renter, one return, exactly once. Violating ownership causes subtle data corruption.

Use Reusable Message Objects Carefully

For reference-heavy objects, reuse can work with ObjectPool from Microsoft.Extensions.ObjectPool.

csharp
1using Microsoft.Extensions.ObjectPool;
2
3public sealed class WorkItem
4{
5    public int Id;
6    public byte[] Data = new byte[1024];
7
8    public void Reset()
9    {
10        Id = 0;
11        Array.Clear(Data, 0, Data.Length);
12    }
13}
14
15var provider = new DefaultObjectPoolProvider();
16var pool = provider.Create<WorkItem>();
17
18WorkItem item = pool.Get();
19try
20{
21    item.Id = 42;
22    // process
23}
24finally
25{
26    item.Reset();
27    pool.Return(item);
28}

Object pools help only when object setup cost is high enough to justify complexity.

Choose Value Types for Tiny Metadata

Small immutable metadata can use value types to reduce reference allocations.

csharp
public readonly record struct QueueToken(int Id, int Partition);

Do not make large structs in hot paths, because copy overhead can offset allocation savings.

Build an End-to-End Low-Allocation Loop

Combine bounded channel, pooling, and explicit cancellation:

csharp
1using var cts = new CancellationTokenSource();
2
3var ch = Channel.CreateBounded<QueueToken>(1024);
4
5Task writer = Task.Run(async () =>
6{
7    for (int i = 0; i < 10_000; i++)
8    {
9        await ch.Writer.WriteAsync(new QueueToken(i, i % 4), cts.Token);
10    }
11    ch.Writer.Complete();
12});
13
14Task reader = Task.Run(async () =>
15{
16    await foreach (var token in ch.Reader.ReadAllAsync(cts.Token))
17    {
18        byte[] rented = ArrayPool<byte>.Shared.Rent(256);
19        try
20        {
21            rented[0] = (byte)token.Partition;
22        }
23        finally
24        {
25            ArrayPool<byte>.Shared.Return(rented);
26        }
27    }
28});
29
30await Task.WhenAll(writer, reader);

This pattern keeps memory bounded and supports orderly shutdown.

Validate with Realistic Benchmarks

Compare designs under burst and steady traffic. Include payload size, percentiles for queue latency, and GC pause behavior. Average throughput alone can hide serious tail latency regressions.

A queue strategy is successful only if it improves total service behavior, not just microbenchmark operations per second.

Common Pitfalls

  • Chasing queue replacement without profiling the full pipeline.
  • Using unbounded queues and exhausting memory under load spikes.
  • Returning pooled buffers incorrectly or multiple times.
  • Reusing mutable objects without complete reset logic.
  • Measuring only throughput and ignoring p95 and p99 latency.

Summary

  • Allocation pressure is usually pipeline-wide, not queue-only.
  • Bounded Channel is a strong default for async producer and consumer flows.
  • ArrayPool and object pooling can cut heap churn when ownership is explicit.
  • Small value types help for metadata, but large structs can hurt performance.
  • Validate changes with realistic load tests that include latency and GC metrics.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.