ASP.NET Core: Implementing Background Processing with Hosted Services

Not every task should run inside an HTTP request. Sending notifications, processing scheduled work, and performing periodic maintenance may take longer than a request should remain open.

ASP.NET Core hosted services provide a way to run background work as part of an application’s hosting lifecycle.

Why Move Work Out of the Request?

Imagine an endpoint that accepts a report request and immediately generates a large file before returning a response.

The operation may take several seconds or minutes. It consumes request resources and can fail if the client disconnects or the request times out.

For suitable workflows, the endpoint can validate the request, enqueue work, and return a job identifier. A background worker can then process the job independently.

Create a Hosted Service

A BackgroundService is a common base class for long-running hosted tasks.

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public sealed class CleanupWorker : BackgroundService
{
    private readonly ILogger<CleanupWorker> _logger;

    public CleanupWorker(
        ILogger<CleanupWorker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(
            TimeSpan.FromHours(1));

        do
        {
            try
            {
                await CleanupAsync(stoppingToken);
            }
            catch (OperationCanceledException)
                when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception exception)
            {
                _logger.LogError(
                    exception,
                    "Background cleanup failed");
            }
        }
        while (await timer.WaitForNextTickAsync(
            stoppingToken));
    }

    private static Task CleanupAsync(
        CancellationToken cancellationToken)
    {
        // Perform bounded, cancellation-aware cleanup.
        return Task.CompletedTask;
    }
}

Register the service:

builder.Services.AddHostedService<CleanupWorker>();

This example demonstrates periodic execution and basic error handling. The actual cleanup implementation should observe cancellation and avoid holding resources longer than necessary.

Respect Dependency Injection Lifetimes

Hosted services are generally registered as singletons. Do not inject a scoped database context directly into a long-lived worker and retain it indefinitely.

Instead, create a scope for each unit of work:

using var scope = _serviceScopeFactory.CreateScope();

var service = scope.ServiceProvider
    .GetRequiredService<CleanupService>();

await service.RunAsync(stoppingToken);

Inject IServiceScopeFactory into the worker and create scopes as needed. This allows scoped dependencies to be disposed at the end of each operation.

Use Queues for Independent Jobs

A timer is useful for periodic tasks, but many applications need a queue for work submitted by requests.

An in-memory queue can be appropriate for lightweight, noncritical work. However, queued jobs may be lost if the process stops or the application restarts.

For important jobs, use durable storage or a dedicated messaging system, and design processing to tolerate retries and duplicate delivery.

Handle Shutdown and Failure

The host provides a cancellation token when the application is stopping. Pass it to asynchronous operations so work can stop cooperatively.

Do not assume every job will finish before a deployment or machine shutdown. If unfinished work must survive restarts, persist its state and make it recoverable.

Log failures with enough context to investigate them, but avoid logging sensitive job payloads.

Monitor Background Work

Useful metrics include the number of pending jobs, processing duration, failure count, retry count, and age of the oldest queued job.

A worker that remains running but never makes progress can be just as problematic as a crashed worker.

Conclusion

ASP.NET Core hosted services are useful for periodic maintenance and long-running background processing. Use dependency-injection scopes correctly, respect cancellation, and choose durable queues when jobs must survive process restarts.

Background processing is reliable only when its lifecycle, failure behavior, and operational visibility are designed deliberately.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *