Course contentsShow
← C#
Lesson 1545 of 1,90814. Background Work and MessagingFree lesson

Prioritizing Background Tasks

Build a multi-queue system to prioritize urgent tasks over low-priority background maintenance jobs.

In this lesson, you will learn how to architect a multi-queue system that ensures high-priority tasks are executed before routine maintenance background jobs.

When relying on a standard Channel<T> or a single IHostedService loop, all tasks are processed in a "First-In, First-Out" (FIFO) manner. This creates a bottleneck where a backlog of low-priority tasks (like generating daily reports) delays time-sensitive operations (like sending a password reset email). To solve this, we decouple the intake of tasks from their execution using a prioritized dispatch mechanism.

The Multi-Queue Pattern

The most effective way to handle this in C# is by using multiple Channel<T> instances—one for each priority level—and a single BackgroundService that "listens" to them in a specific order of precedence.

public class PrioritizedWorker : BackgroundService
{
    private readonly ChannelReader<WorkItem> _highPriority;
    private readonly ChannelReader<WorkItem> _lowPriority;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Always check the high-priority channel first
            if (_highPriority.TryRead(out var item) || 
                await _highPriority.WaitToReadAsync(stoppingToken))
            {
                await Process(item);
            }
            // Only check the low-priority channel if no high-priority work exists
            else if (_lowPriority.TryRead(out var lowItem) || 
                     await _lowPriority.WaitToReadAsync(stoppingToken))
            {
                await Process(lowItem);
            }
        }
    }
}

Why this works

By structuring your loop this way, the worker remains "greedy" for high-priority items. Even if a thousand low-priority jobs are queued, the ExecuteAsync loop will continuously drain the _highPriority channel until it is empty before ever checking the _lowPriority channel.

This approach provides a clear separation of concerns. The producers of your background tasks don't need to know how the scheduling logic works; they simply inject the appropriate Channel based on the urgency of the request. This avoids the complexity of manual task sorting and ensures your system remains responsive under heavy load without requiring expensive infrastructure changes.

Key Takeaway: Use multiple specialized channels and an prioritized polling loop in your background worker to ensure critical tasks bypass routine maintenance queues.