C#
Asynchronous Programming
Task Queuing
.NET
Concurrency

Queuing asynchronous task in C

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Queuing asynchronous work in C# is not just about launching tasks with Task.Run(). A real queue controls when work starts, how much runs concurrently, and how shutdown or failure is handled. If you want predictable background processing, build an explicit producer-consumer pipeline instead of creating unbounded tasks on demand.

Why a Queue Is Different from Fire-and-Forget

This is not a queue:

csharp
Task.Run(() => DoWorkAsync());
Task.Run(() => DoWorkAsync());
Task.Run(() => DoWorkAsync());

That starts work immediately and provides no real backpressure. A queue, by contrast, stores requests until a consumer is ready to process them.

A Simple Async Queue with Channel

In modern .NET, System.Threading.Channels is a strong choice for asynchronous producer-consumer workflows.

csharp
1using System;
2using System.Threading;
3using System.Threading.Channels;
4using System.Threading.Tasks;
5
6public class BackgroundQueue
7{
8    private readonly Channel<Func<CancellationToken, ValueTask>> _channel =
9        Channel.CreateUnbounded<Func<CancellationToken, ValueTask>>();
10
11    public ValueTask QueueAsync(Func<CancellationToken, ValueTask> workItem)
12    {
13        return _channel.Writer.WriteAsync(workItem);
14    }
15
16    public async Task RunAsync(CancellationToken token)
17    {
18        await foreach (var workItem in _channel.Reader.ReadAllAsync(token))
19        {
20            await workItem(token);
21        }
22    }
23}

This gives you a real asynchronous queue with clear producer and consumer roles.

Enqueueing Work

You can now push units of work into the queue without starting them immediately in arbitrary parallel bursts.

csharp
1var queue = new BackgroundQueue();
2var cts = new CancellationTokenSource();
3
4await queue.QueueAsync(async token =>
5{
6    await Task.Delay(500, token);
7    Console.WriteLine("job 1 finished");
8});
9
10await queue.QueueAsync(async token =>
11{
12    await Task.Delay(200, token);
13    Console.WriteLine("job 2 finished");
14});
15
16var worker = queue.RunAsync(cts.Token);

In a real application, RunAsync would usually start once during service startup and keep draining the queue until shutdown.

Add Concurrency Control When Needed

Some systems want a queue, but not strictly one-at-a-time processing. Use SemaphoreSlim if you need bounded concurrency.

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Threading;
4using System.Threading.Tasks;
5
6public class LimitedRunner
7{
8    private readonly SemaphoreSlim _semaphore = new(3);
9
10    public async Task RunAsync(Func<Task> work)
11    {
12        await _semaphore.WaitAsync();
13        try
14        {
15            await work();
16        }
17        finally
18        {
19            _semaphore.Release();
20        }
21    }
22}

This is useful when jobs are independent but you still need to protect the database, API, or CPU from overload.

Bounded Channels Prevent Unbounded Memory Growth

An unbounded queue is simple, but it can hide overload until memory usage becomes the problem. If producers can outpace consumers for long periods, prefer a bounded channel.

csharp
1private readonly Channel<Func<CancellationToken, ValueTask>> _channel =
2    Channel.CreateBounded<Func<CancellationToken, ValueTask>>(
3        new BoundedChannelOptions(100)
4        {
5            FullMode = BoundedChannelFullMode.Wait
6        });

With this setup, writers naturally slow down when the queue reaches capacity. That is often the right kind of backpressure for web servers and background services.

Error Handling Strategy

A queue must define what happens when a job fails. At minimum:

  • log the exception
  • decide whether to retry
  • avoid crashing the whole worker loop unless that is intentional

Example consumer loop with isolated failure handling:

csharp
1public async Task RunAsync(CancellationToken token)
2{
3    await foreach (var workItem in _channel.Reader.ReadAllAsync(token))
4    {
5        try
6        {
7            await workItem(token);
8        }
9        catch (Exception ex)
10        {
11            Console.WriteLine($"queue item failed: {ex.Message}");
12        }
13    }
14}

Without that boundary, one bad work item can stop the queue completely.

Shutdown and Cancellation

Background queues need a stop story. Cancellation should tell producers to stop enqueuing and tell consumers to stop reading or finish in-flight work cleanly.

This is why CancellationToken should be part of the queue contract rather than an afterthought. A queue that cannot stop cleanly becomes a deployment and shutdown problem later.

When a Full Queue Abstraction Is Worth It

Use a real async queue when:

  • jobs arrive faster than they should execute
  • you need central retry or throttling policy
  • you need predictable shutdown behavior
  • several parts of the app submit background work

If the application only has one or two awaited operations, a queue may be unnecessary. Do not build one unless the workload actually needs producer-consumer semantics.

In ASP.NET Core, this pattern is often wrapped in a hosted service. The queue accepts work from controllers or application services, and a single long-running background worker drains it. That keeps request threads responsive without losing control over background execution.

Common Pitfalls

  • Treating repeated Task.Run() calls as if they were a controlled queue.
  • Allowing unlimited concurrency and overwhelming the downstream resource.
  • Forgetting to define what happens when one queued task throws.
  • Omitting cancellation and then hanging during shutdown.
  • Building a background queue when a simple awaited method call would be enough.

Summary

  • Queuing async work means buffering and controlling execution, not just launching tasks.
  • 'Channel is a good fit for asynchronous producer-consumer pipelines in modern .NET.'
  • Add concurrency limits explicitly when the workload should not run all at once.
  • Handle exceptions inside the consumer loop so one failure does not kill the queue.
  • Design cancellation and shutdown behavior from the start.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

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

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.