When building modern web applications, certain operations—such as generating end-of-month financial reports, synchronizing external inventory feeds, sending bulk notification emails, or processing video transcodes—take minutes or hours to execute.
If you execute these operations directly inside an HTTP request handler, your web thread blocks, connection pools exhaust, HTTP timeouts trigger, and client requests fail.
Background Services (IHostedService / BackgroundService) solve this by running long-running asynchronous tasks directly within the ASP.NET Core process lifecycle, independent of incoming HTTP requests.
This article walks through a complete, production-grade implementation of background task processing in an ASP.NET Core Web API using BackgroundService combined with thread-safe System.Threading.Channels queues.
1. Architectural Strategy: Queues & Worker Lifecycles
To build a reliable background processor, follow a decoupled producer-consumer pattern:
The Producer (API Controller): When an expensive task is requested, the controller drops a lightweight message payload into an in-memory thread-safe queue and returns an immediate
202 Acceptedresponse.The Consumer (Background Worker): A persistent background worker continuously polls the queue, pulling items off one-by-one and executing them in the background.
Graceful Shutdown: When the application recycles or stops for deployment, the background worker finishes its current iteration safely before shutting down.
2. Step 1: Creating a Thread-Safe Message Queue
Standard collections like List<T> or Queue<T> are not thread-safe and will corrupt memory when accessed concurrently by web request threads and background worker threads.
Instead, use System.Threading.Channels, which is optimized for high-throughput, lock-free, asynchronous producer-consumer messaging.
C#
// Application/BackgroundJobs/TaskQueue.cs
using System.Threading.Channels;
namespace Application.BackgroundJobs;
public interface ITaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, Task> workItem);
ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}
public class TaskQueue : ITaskQueue
{
private readonly Channel<Func<CancellationToken, Task>> _queue;
public TaskQueue(int capacity = 100)
{
// Bounded channel prevents memory overflow under heavy traffic floods
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options);
}
public async ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, Task> workItem)
{
if (workItem == null) throw new ArgumentNullException(nameof(workItem));
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
{
return await _queue.Reader.ReadAsync(cancellationToken);
}
}
3. Step 2: Implementing the BackgroundService Worker
Create the persistent worker that inherits from ASP.NET Core's abstract BackgroundService base class.
C#
// Infrastructure/BackgroundJobs/QueuedHostedService.cs
using Application.BackgroundJobs;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Infrastructure.BackgroundJobs;
public class QueuedHostedService : BackgroundService
{
private readonly ITaskQueue _taskQueue;
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(ITaskQueue taskQueue, ILogger<QueuedHostedService> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is running.");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Pull next background task from channel queue
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
// Execute the task asynchronously
await workItem(stoppingToken);
}
catch (OperationCanceledException)
{
// Prevent crash during graceful application shutdown
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred executing background work item.");
}
}
_logger.LogInformation("Queued Hosted Service is stopping.");
}
}
4. Step 3: Registering Services in Program.cs
Register your task queue as a singleton so all controllers share the same queue instance, and register your hosted service as a background worker.
C#
// WebApi/Program.cs
using Application.BackgroundJobs;
using Infrastructure.BackgroundJobs;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register Singleton Task Queue & Hosted Background Worker
builder.Services.AddSingleton<ITaskQueue, TaskQueue>();
builder.Services.AddHostedService<QueuedHostedService>();
var app = builder.Build();
// ... configure middleware pipeline
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
5. Step 4: Dispatching Jobs from API Controllers
Inject ITaskQueue into your controller, queue the work item, and return an immediate response to the client.
C#
// WebApi/Controllers/ReportsController.cs
using Application.BackgroundJobs;
using Microsoft.AspNetCore.Mvc;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
private readonly ITaskQueue _taskQueue;
private readonly ILogger<ReportsController> _logger;
public ReportsController(ITaskQueue taskQueue, ILogger<ReportsController> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}
[HttpPost("generate")]
public async Task<IActionResult> GenerateReport([FromBody] string reportType)
{
// Enqueue the long-running task safely without blocking the HTTP thread
await _taskQueue.QueueBackgroundWorkItemAsync(async token =>
{
_logger.LogInformation("Starting background generation for report: {ReportType}", reportType);
// Simulate expensive work (e.g., crunching database records or rendering PDF)
await Task.Delay(TimeSpan.FromSeconds(10), token);
_logger.LogInformation("Successfully completed generation for report: {ReportType}", reportType);
});
// Return instant response confirming task has been accepted for processing
return Accepted(new { message = "Report generation has been queued successfully." });
}
}
Conclusion
By leveraging BackgroundService and thread-safe System.Threading.Channels queues in ASP.NET Core, your API avoids request timeouts and thread exhaustion, safely handling long-running enterprise jobs in the background.

Join the conversation! Your thoughts help the community grow.