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)
ReadAllAsyncrespects 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#