Skip to content

System.Threading.Channels

Overview

System.Threading.Channels is a high-performance, thread-safe, producer–consumer queue abstraction in .NET. It provides asynchronous, bounded/unbounded, single/multi-producer, single/multi-consumer channels optimised for high throughput with low allocation.

Channels are heavily used in:

  • Background processing
  • Actor-based workflows
  • Pipelines
  • Streaming data
  • High-throughput I/O systems
  • Scheduling, messaging, and job queues

Channels were introduced in .NET Core 3.0

Use Cases

Channels solve common producer–consumer challenges:

  • Avoid manual locks
  • Prevent blocking threads unnecessarily
  • Control memory/pressure via bounded capacity
  • Provide backpressure (slow consumer blocks producer)
  • Work naturally with async/await
  • Extremely fast vs alternatives like BlockingCollection<T>

Architecture Diagram

flowchart LR
    P1[Producer 1] --> W[ChannelWriter<T>]
    P2[Producer 2] --> W
    W --> C[Channel<T> Pipeline]
    C --> R[ChannelReader<T>]
    R --> Consumer1
    R --> Consumer2

A Channel<T> exposes:

  • Channel.Writer (ChannelWriter<T>) — producers write messages
  • Channel.Reader (ChannelReader<T>) — consumers read messages

The channel controls buffering, backpressure, and concurrency rules.

Types of Channels

Unbounded Channels

var channel = Channel.CreateUnbounded<int>();
  • No limit on messages.
  • Fastest and simplest behaviour.

Bounded Channels

var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(100)
{
    SingleWriter = false,
    SingleReader = false,
    FullMode = BoundedChannelFullMode.Wait
});

Useful when:

  • Preventing memory blow-up.
  • Implementing backpressure.
  • You know maximum parallel throughput.

FullMode options:

Mode Description
Wait Producer waits when channel is full (recommended)
DropWrite Drop new item if full
DropOldest Remove oldest item to make space
DropNewest Drop most recent message

Single Reader / Single Writer Channels

Optimised variants:

var channel = Channel.CreateUnbounded<int>(
    new UnboundedChannelOptions { SingleReader = true, SingleWriter = true }
);

When you know usage patterns, these are significantly faster.

Buffered vs Unbuffered

  • Buffered: holds N items before applying backpressure
  • Unbuffered: writer waits for reader (CreateUnbuffered())

Basic Example — Producer/Consumer

Producer

var channel = Channel.CreateUnbounded<int>();

var producer = Task.Run(async () =>
{
    for (int i = 0; i < 10; i++)
    {
        await channel.Writer.WriteAsync(i);
    }
    channel.Writer.Complete();
});

Consumer

var consumer = Task.Run(async () =>
{
    await foreach (var value in channel.Reader.ReadAllAsync())
    {
        Console.WriteLine($"Consumed: {value}");
    }
});

This pattern gives you:

  • Non-blocking I/O
  • Automatic backpressure when bounded
  • Clean completion handling

Advanced Example — Background Worker Pipeline

Great pattern for .NET APIs, microservices, or background workers.

public class ProcessingPipeline
{
    private readonly Channel<string> _channel;

    public ProcessingPipeline()
    {
        _channel = Channel.CreateBounded<string>(100);
        StartConsumers();
    }

    public ValueTask EnqueueAsync(string message)
        => _channel.Writer.WriteAsync(message);

    private void StartConsumers()
    {
        for (int i = 0; i < 3; i++)
        {
            _ = Task.Run(async () =>
            {
                await foreach (var msg in _channel.Reader.ReadAllAsync())
                {
                    await ProcessAsync(msg);
                }
            });
        }
    }

    private Task ProcessAsync(string msg)
    {
        // Application logic
        return Task.Delay(50);
    }
}

Example API implementation

You might implement in an API via DI as follows.

Define the message, e.g. in ThumbnailGenerator.cs

public record ThumbnailGenerationJob(string Id, string OriginalFilePath, string FolderPath);

Register the channel in Program.cs

builder.Services.AddSingleton(_ =>
{
    var channel = Channel.CreateBounded<ThumbnailGenerationJob>(new BoundedChannelOptions(100)
    {
        FullMode = BoundedChannelFullMode.Wait
    });

    return channel;
});

Inject channel and write to the channel in a class or controller

public sealed class ThumbnailsController(
    ...,
    Channel<ThumbnailGenerationJob> channel,
    ...) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> UploadImage(IFormFile? file)
    {
       ...

        var id = Guid.NewGuid().ToString();
        var folderPath = Path.Combine(_uploadDirectory, "images", id);
        var fileName = $"{id}{Path.GetExtension(file.FileName)}";

        var originalFilePath = await imageService.SaveOriginalImageAsync(file, folderPath, fileName);

        var job = new ThumbnailGenerationJob(id, originalFilePath, folderPath);
        await channel.Writer.WriteAsync(job);

       ...
        return Accepted(statusUrl, new { id, status = ThumbnailGenerationStatus.Queued });
    }
}

Inject the channel and consume in a service

public class ThumbnailGenerationService(
    ...,
    Channel<ThumbnailGenerationJob> channel,
    ...) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in channel.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                await ProcessJobAsync(job);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error processing thumbnail generation job");
            }
        }
    }
}

Comparison with Alternatives

Channels vs BlockingCollection

Aspect Channels BlockingCollection
Async support ✔ Native ✘ Requires manual setup
Throughput High Moderate
Backpressure ✔ Bounded + Wait ✔ but less configurable
Legacy? Modern Legacy (pre .NET Core)

Channels vs ConcurrentQueue

Aspect Channels ConcurrentQueue
Blocking / waiting
Async operations
Backpressure
Signalling consumer Built-in Manual required

Channels vs TPL Dataflow

Aspect Channels TPL Dataflow
Ease of use Simple More complex
Full pipeline features Limited Complete (batching, chunking, linking)
Overhead Low Medium

Channels offer a lighter, faster, simpler alternative to Dataflow when you don’t need advanced block chaining.

When to Use Channels

Perfect for

  • High‐throughput producer/consumer pipelines
  • Background workers
  • Web API → background processing patterns
  • Actor models
  • Stream processing (e.g., Kafka consumers → processing queue)
  • Job queues inside .NET services
  • Concurrency optimisation without locks

Avoid when

  • You need distributed messaging (use Kafka, Azure Service Bus)
  • You need durability on crash
  • You need cross-process communication

Channels are in-memory, not durable.

Error Handling

Handling completion

channel.Writer.Complete(new Exception("Something went wrong"));

Reader side

try
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        // ...
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Pipeline failed: {ex}");
}

Performance Notes (.NET 10)

  • Channels are lock-minimised, using queues and semaphores internally.
  • SingleReader and SingleWriter channels eliminate unnecessary locking.
  • Performance significantly improved in .NET 7+ and .NET 10.

Benchmarks show channels are often 2–5x faster than BlockingCollection.

Summary

System.Threading.Channels is a high-performance, thread-safe producer–consumer abstraction introduced in .NET Core.

Channels support async writes/reads, bounded/unbounded buffering, backpressure, and lock-efficient concurrency optimisations**. It is ideal for high-throughput pipelines, background processing, actors, and message workflows inside a .NET service.

Channels expose a ChannelWriter and ChannelReader to allow multiple producers and consumers. They can be tuned for single-writer/single-reader scenarios for extra performance. Compared to alternatives like BlockingCollection or ConcurrentQueue, channels provide better async support, predictable throughput, and modern patterns aligned with async/await.

Use channels when you need in-memory, high-speed, async, producer-consumer coordination.

Further Reading / References


Definitions

Term Description
Backpressure Refers to the natural throttling that happens when a consumer can’t keep up with the producer, causing the channel to slow or block the producer. Essentially once the capacity of a channel is met, no more messages can be added until one is removed. This prevents uncontrolled memory growth, CPU exhaustion and runaway production that the consumer cannot process, but it does slow the writer side