In modern applications, especially web and desktop apps, executing long-running tasks on the main thread can block user interactions or degrade performance. That’s where background tasks come in.
In this article, we’ll explore the different ways to run tasks in the background in C#, including:
-
Task.Run
for simple async operations. -
BackgroundService
for long-running services in ASP.NET Core. -
IHostedService
for more controlled services. - Queued background tasks using
IBackgroundTaskQueue
.
We’ll also provide a working example you can use as a starting point in your projects.
đź§ Why Use Background Tasks?
Background tasks are useful for:
- Offloading long-running operations (e.g. report generation, image processing).
- Avoiding blocking the main thread in APIs or UI apps.
- Running scheduled or periodic jobs (e.g. sending emails, syncing data).
- Handling async operations in a scalable way.
🔹 1. Using Task.Run
for Lightweight Background Work
If you need to run a simple piece of work in the background, Task.Run
is the quickest way:
public async Task ProcessImageAsync(string imagePath)
{
await Task.Run(() =>
{
// Simulate long processing
Thread.Sleep(2000);
Console.WriteLine($"Processed image: {imagePath}");
});
}
⚠️ Note: Avoid using Task.Run
for CPU-intensive or long operations in ASP.NET apps—it's better suited for quick, non-blocking tasks.
🔹 2. Creating a Background Service with BackgroundService
For more robust needs, like polling or continuous background work in ASP.NET Core, use BackgroundService
.
public class WorkerService : BackgroundService
{
private readonly ILogger<WorkerService> _logger;
public WorkerService(ILogger<WorkerService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(5000, stoppingToken);
}
}
}
Register it in Program.cs
:
builder.Services.AddHostedService<WorkerService>();
🔹 3. Implementing IHostedService
Manually
If you need more control over lifecycle methods, implement IHostedService
directly:
public class TimedService : IHostedService, IDisposable
{
private Timer _timer;
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object state)
{
Console.WriteLine($"Work executed at {DateTime.Now}");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
🔹 4. Queued Background Tasks with Channels
You can create a queue of background tasks using a producer-consumer pattern.
Interface:
public interface IBackgroundTaskQueue
{
void Enqueue(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}
Implementation:
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, Task>> _queue = Channel.CreateUnbounded<Func<CancellationToken, Task>>();
public void Enqueue(Func<CancellationToken, Task> workItem)
{
_queue.Writer.TryWrite(workItem);
}
public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
return await _queue.Reader.ReadAsync(cancellationToken);
}
}
Worker:
public class QueuedHostedService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
public QueuedHostedService(IBackgroundTaskQueue taskQueue)
{
_taskQueue = taskQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
await workItem(stoppingToken);
}
}
}
Register in Program.cs
:
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
builder.Services.AddHostedService<QueuedHostedService>();
Use in API Endpoint:
app.MapPost("/enqueue", (IBackgroundTaskQueue queue) =>
{
queue.Enqueue(async token =>
{
await Task.Delay(3000, token);
Console.WriteLine("Background task finished.");
});
return Results.Ok("Task enqueued.");
});
⚠️ Common Pitfalls
- ❌ Blocking the thread: Don’t use
Thread.Sleep
in async methods—useawait Task.Delay
. - ❌ Using Task.Run in ASP.NET: Avoid it in web apps unless absolutely necessary—it uses up valuable thread pool resources.
- ❌ Forgetting cancellation: Always respect
CancellationToken
in long-running tasks.
âś… Best Practices
- Use
BackgroundService
orIHostedService
for background workers. - Use dependency injection to manage services within tasks.
- Respect cancellation tokens to allow graceful shutdown.
- Log exceptions in background tasks to avoid silent failures.
- Monitor performance and queue length in production.
đź”— Useful Links
- .NET BackgroundService Docs
- Queued Background Tasks in ASP.NET Core
- Microsoft.Extensions.Hosting NuGet package
For the full code and examples, visit my GitHub repository.
Top comments (2)
Been messing with background tasks myself - this helped me clear some stuff up quick. Cheers!
I've used these features extensively, and they've worked great , especially channels, which are one of my favorites.
Thanks for sharing!