
What Are Background Services?
Not every task in an ASP.NET Core application needs to happen while a user is waiting for an HTTP response. Some operations need to run in the background, either continuously or at scheduled intervals.
Examples include processing messages from a queue, synchronising data with another system, cleaning up old records, sending notifications, generating reports and performing regular maintenance tasks.
ASP.NET Core provides built-in support for this type of work through hosted services. These services run alongside the rest of your application and are managed by the ASP.NET Core hosting infrastructure.
The most common way to create a background service is by inheriting from the BackgroundService class.
Why Use a Background Service?
Imagine an API endpoint that needs to generate a large report. If the API generates the entire report before returning a response, the user may have to wait several seconds or even minutes.
A better approach could be to accept the request, place a message on a queue and return immediately. A background service can then process that queue independently.
This approach keeps HTTP requests responsive and separates the responsibility of accepting a request from the responsibility of performing potentially long-running work.
Background services are particularly useful when the work does not need to be completed before the user receives a response.
Creating Your First Background Service
Creating a background service is relatively straightforward.
The first step is to create a class that inherits from BackgroundService. The BackgroundService class is part of the Microsoft.Extensions.Hosting namespace and provides the ExecuteAsync method that your service will use.
A simple example looks like this:
using Microsoft.Extensions.Hosting;
public class MyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine(
$"Background service running: {DateTime.Now}");
await Task.Delay(
TimeSpan.FromSeconds(10),
stoppingToken);
}
}
}
The ExecuteAsync method is called when the hosted service starts.
The loop continues running until the application is shutting down. The CancellationToken allows the service to respond to the application's shutdown process rather than continuing to run indefinitely.
The Task.Delay call prevents the loop from continuously consuming CPU resources.
Registering the Background Service
Creating the service is only part of the process. ASP.NET Core also needs to know that the service should be started when the application starts.
This can be done in Program.cs using AddHostedService.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddHostedService<MyBackgroundService>(); var app = builder.Build(); app.MapControllers(); app.Run();
When the application starts, ASP.NET Core creates an instance of MyBackgroundService and manages its lifetime.
When the application shuts down, the cancellation token is triggered, allowing the background service to finish gracefully.
Using Dependency Injection
One of the biggest advantages of using BackgroundService is that it integrates with ASP.NET Core's dependency injection system.
This means your background service can use other application services, repositories and database contexts.
For example, you might inject a service responsible for processing orders.
public class OrderBackgroundService : BackgroundService
{
private readonly IOrderProcessor _orderProcessor;
public OrderBackgroundService(
IOrderProcessor orderProcessor)
{
_orderProcessor = orderProcessor;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _orderProcessor.ProcessOrdersAsync(
stoppingToken);
await Task.Delay(
TimeSpan.FromSeconds(30),
stoppingToken);
}
}
}
The dependency is registered in the normal way.
builder.Services.AddScoped<IOrderProcessor, OrderProcessor>(); builder.Services.AddHostedService<OrderBackgroundService>();
However, there is an important consideration here. Background services are registered as singletons by the hosting infrastructure, so you should not directly inject a scoped service into a BackgroundService.
This is particularly important when working with Entity Framework Core DbContext instances, which are normally registered as scoped services.
Creating a Dependency Injection Scope
If your background service needs to use scoped dependencies, you should create a scope using IServiceScopeFactory.
For example:
public class OrderBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderBackgroundService(
IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var processor =
scope.ServiceProvider
.GetRequiredService<IOrderProcessor>();
await processor.ProcessOrdersAsync(
stoppingToken);
await Task.Delay(
TimeSpan.FromSeconds(30),
stoppingToken);
}
}
}
Each iteration creates a new dependency injection scope. This allows scoped services to be created and disposed correctly.
This pattern is especially useful when a background service interacts with a database.
Background Services and Entity Framework Core
A common use for background services is performing database operations.
For example, you might have a service that periodically checks for orders that need processing.
The background service itself should not hold onto a DbContext indefinitely. DbContext instances are designed to represent a unit of work and should generally have a controlled lifetime.
Using IServiceScopeFactory allows a new DbContext to be created for each unit of work.
using var scope = _scopeFactory.CreateScope();
var dbContext =
scope.ServiceProvider
.GetRequiredService<ApplicationDbContext>();
var orders = await dbContext.Orders
.Where(x => !x.Processed)
.ToListAsync(stoppingToken);
Once the scope is disposed, the DbContext is disposed as well.
This keeps the lifetime of your database connection and tracking information under control.
Running a Task at Regular Intervals
One of the simplest uses for a background service is running a task at regular intervals.
The service can perform its work and then wait before running again.
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await PerformWorkAsync(stoppingToken);
await Task.Delay(
TimeSpan.FromMinutes(5),
stoppingToken);
}
}
This approach works well for simple applications where a task needs to run every few minutes.
For example, you might use it to remove expired sessions, process pending records or synchronise data.
However, it is important to understand that Task.Delay does not guarantee that your task will run at precisely the expected time. The application may be restarted, paused or unavailable.
If the task needs to run at a specific time every day, you may need a more sophisticated scheduling solution.
Handling Cancellation Correctly
A background service should always respect the cancellation token supplied by ASP.NET Core.
The application may need to shut down because the server is restarting, the application is being redeployed or the hosting environment is stopping the process.
The cancellation token allows your service to stop gracefully.
For example:
while (!stoppingToken.IsCancellationRequested)
{
await ProcessDataAsync(stoppingToken);
await Task.Delay(
TimeSpan.FromMinutes(1),
stoppingToken);
}
Passing the cancellation token into asynchronous operations is important.
For example:
await dbContext.SaveChangesAsync(stoppingToken);
This allows the operation to be cancelled when the application is shutting down.
You should avoid ignoring cancellation requests, particularly when your service performs long-running operations.
Handling Exceptions
Exceptions in background services need careful consideration.
If an exception escapes from ExecuteAsync, the behaviour of the hosted service depends on the hosting configuration and .NET version. An unhandled exception can cause the background service to stop, and in some configurations it can also cause the entire host application to stop.
For this reason, long-running background services should generally have appropriate exception handling and logging.
For example:
try
{
await ProcessDataAsync(stoppingToken);
}
catch (OperationCanceledException)
when (stoppingToken.IsCancellationRequested)
{
// Application is shutting down.
}
catch (Exception ex)
{
_logger.LogError(
ex,
"An error occurred while processing background work.");
}
The OperationCanceledException is treated differently because it is expected when the application is shutting down.
Other exceptions can be logged so that they can be investigated.
You should also consider whether the service should retry the operation after a failure. In some situations, a retry policy can be useful. In others, repeatedly retrying a failed operation could make the problem worse.
Logging Background Services
Logging is particularly important for background services because there is no user sitting in front of the application waiting for an error message.
Using ILogger allows you to record when the service starts, when work is processed and when errors occur.
private readonly ILogger<MyBackgroundService> _logger;
public MyBackgroundService(
ILogger<MyBackgroundService> logger)
{
_logger = logger;
}
You can then log information from within the service.
_logger.LogInformation(
"Background service is processing pending orders.");
For errors, include the exception where possible.
_logger.LogError(
ex,
"An error occurred while processing pending orders.");
Good logging makes it much easier to understand what a background service was doing when something went wrong.
Background Services in Web Applications
There is an important distinction between a background service running inside an ASP.NET Core application and a dedicated worker application.
An ASP.NET Core application can host background services alongside its web APIs and MVC or Blazor components. This can be convenient because everything is deployed together.
However, the background service is tied to the lifetime of the web application.
If the application stops, the background service stops too.
This means that background services running inside a web application are not always suitable for mission-critical or long-running workloads where the work must continue regardless of the web application's availability.
For larger systems, it may be better to create a separate .NET Worker Service.
Background Services and Queues
A common architecture is to combine a background service with a queue.
Instead of performing work during an HTTP request, the API places a job onto a queue.
The background service then reads jobs from the queue and processes them.
For example, an API might receive a request to generate a report. Rather than generating the report immediately, it could create a job containing the required information.
The background service picks up the job and performs the processing independently.
This architecture has several advantages. HTTP requests remain fast, work can be processed asynchronously and the system can be designed to handle larger workloads.
For production applications, you may use an external message broker or queueing system rather than an in-memory queue.
The choice depends on the reliability and scale requirements of your application.
When Should You Use a Background Service?
Background services are a good choice when you have work that can happen independently of an HTTP request.
They are particularly useful for periodic maintenance, processing queues, synchronising data, sending notifications and performing other tasks that do not need to block a user's request.
They are less suitable when the work must survive application restarts without losing its state, or when the workload requires advanced scheduling, distributed processing or guaranteed delivery.
In those cases, a dedicated worker service, message queue or job scheduling platform may be more appropriate.
Things to Consider in Production
A background service can look very simple when running on a development machine, but production environments introduce additional considerations.
Applications can restart unexpectedly. Servers can be updated. Containers can be replaced. Multiple instances of the same application can run at the same time.
If your application runs on multiple servers, each instance may start its own copy of the background service.
For example, if you have three web application instances and your background service sends a daily email, you could accidentally send the email three times.
This means that distributed applications often need additional coordination, such as distributed locks, database-based coordination or an external job-processing system.
You should also consider what happens if a service stops halfway through processing a task. Ideally, the work should be designed so that it can be safely retried without causing duplicate or corrupted results.
Background Services vs Scheduled Jobs
It is tempting to use BackgroundService for every scheduled task, but it is not always the best solution.
A simple BackgroundService is ideal for straightforward recurring work. However, if you need jobs to run at specific times, maintain job history, retry failed jobs or manage multiple scheduled tasks, a dedicated scheduling library or external service may be more suitable.
The important thing is to choose the simplest architecture that meets the application's requirements.
For a small ASP.NET Core application, a BackgroundService may be all you need.
For a larger application with complex scheduling and reliability requirements, a dedicated job-processing solution is likely to be a better choice.
Final Thoughts
Background services are a powerful feature of ASP.NET Core and provide a straightforward way to perform work outside the normal HTTP request and response cycle.
The BackgroundService class makes it easy to create long-running processes, while ASP.NET Core's dependency injection system allows these services to use the same application infrastructure as the rest of your code.
The key is to use them appropriately. Always consider cancellation, exception handling, logging, dependency lifetimes and application restarts. If your application grows into a distributed system, you should also think carefully about duplicate processing and reliable job delivery.
For simple recurring tasks and lightweight background processing, BackgroundService is an excellent addition to the ASP.NET Core developer's toolkit. When the requirements become more complex, however, it is worth considering a dedicated worker service or job-processing solution.
Become a member
Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!
Read next
Minimal APIs vs Controllers: A Real Comparison
Minimal APIs and Controllers both provide powerful ways to build HTTP APIs with ASP.NET Core, but they take very different approaches. Minimal APIs focus on simplicity and reducing ceremony, while Controllers offer a structured approach that can be better suited to larger applications. In this article, we take a practical look at both approaches and compare their strengths, weaknesses and ideal use cases.
Handling Soft Deletes in EF Core
Tracking vs No-Tracking Queries Explained
