DEV Community

Adrián Bailador
Adrián Bailador

Posted on

Background Tasks in C# đź§µ

Image description

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}");
    });
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in Program.cs:

builder.Services.AddHostedService<WorkerService>();
Enter fullscreen mode Exit fullscreen mode

🔹 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Register in Program.cs:

builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
builder.Services.AddHostedService<QueuedHostedService>();
Enter fullscreen mode Exit fullscreen mode

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.");
});
Enter fullscreen mode Exit fullscreen mode

⚠️ Common Pitfalls

  • ❌ Blocking the thread: Don’t use Thread.Sleep in async methods—use await 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 or IHostedService 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


For the full code and examples, visit my GitHub repository.


Top comments (2)

Collapse
 
nevodavid profile image
Nevo David

Been messing with background tasks myself - this helped me clear some stuff up quick. Cheers!

Collapse
 
stevsharp profile image
Spyros Ponaris

I've used these features extensively, and they've worked great , especially channels, which are one of my favorites.
Thanks for sharing!