Channel-Based Background Workers in .NET

Replacing Task.Run fire-and-forget with System.Threading.Channels — backpressure, clean shutdown, metrics.

Fire-and-forget Task.Run caused unbounded memory growth when downstream slowed.

Before

_ = Task.Run(() => ProcessWebhook(payload)); // no backpressure, no visibility

After

private readonly Channel<WebhookPayload> _queue =
    Channel.CreateBounded<WebhookPayload>(new BoundedChannelOptions(1000)
    {
        FullMode = BoundedChannelFullMode.Wait
    });

// Producer
await _queue.Writer.WriteAsync(payload, ct);

// Consumer (BackgroundService)
await foreach (var item in _queue.Reader.ReadAllAsync(stoppingToken))
    await ProcessWebhook(item);

Benefits

  • Bounded queue = natural backpressure (HTTP 503 when full, client retries)
  • ReadAllAsync respects cancellation on shutdown
  • Queue depth exported to Prometheus — alert at 80% capacity

Throughput unchanged. OOM incidents: zero since migration.

channels , background-services , concurrency · .NET , C#

More in .NET