Running background tasks in asp.net core
A small overview of the standard tools for launching background tasks in aspnet applications – what is, how it differs, how to use it. The built-in mechanism for launching such tasks is built around the interface IHostedService and an extension method for IServiceCollection – AddHostedService. But there are several ways to implement background tasks through this mechanism (and a few more non-obvious moments of the behavior of this mechanism).

Why run background tasks
There are 2 global background task scripts:
One-shot launch of a background task at application startup, waiting for requests to start processing or before. For example, you can migrate data before processing requests, warm up caches or other parts of the application to solve the problem of processing the first requests. You may also need to wait for the application to start – in order to light the beacon of the service in service discovery, having received information about the listening addresses
Periodic regular launch of a background task – this can be a health check, sending service telemetry, cache invalidation
aspnet allows you to separate background services and reuse them in different applications, which will provide a universal mechanism for such operations in different services. All of the examples below use the method to register a new background service. ServiceCollectionHostedServiceExtensions.AddHostedService
:
services.AddHostedService<MyHostedService>();
Custom implementation of IHostedService
Interface IHostedService provides 2 methods:
public interface IHostedService
{
// Вызывается, когда приложение готово запустить фоновую службу
Task StartAsync(CancellationToken stoppingToken);
// Вызывается, когда происходит нормальное завершение работы узла приложения.
Task StopAsync(CancellationToken stoppingToken);
}
What is important to know when implementing an interface? Everything IHostedService
run sequentially, and the call StartAsync
blocks the rest of the application from starting. Therefore, in StartAsync
There should be no long blocking operations, unless you really want to delay application startup until the operation is complete (for example, when migrating a database):

This is precisely the feature and motivation to implement background operations through your own implementation. IHostedService
– if you need full control over starting and stopping a background service. If this is not so important, then it is enough to inherit from the class BackgroundService
.
A couple more important points to implement:
At
CancellationToken
inStopAsync
have 5 seconds to complete correctlyStopAsync may not be called at all when the application terminates unexpectedly. Therefore, for example, it is not enough to extinguish the beacon only in this method.
The general implementation of a background periodic task in this case might look something like this:
public class MyHostedService : IHostedService
{
private readonly ISomeBusinessLogicService someService;
public MyHostedService(ISomeBusinessLogicService someService)
{
this.someService = someService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Не блокируем поток выполнения: StartAsync должен запустить выполнение фоновой задачи и завершить работу
DoSomeWorkEveryFiveSecondsAsync(cancellationToken);
return Task.CompletedTask;
}
private async Task DoSomeWorkEveryFiveSecondsAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await someService.DoSomeWorkAsync();
}
catch (Exception ex)
{
// обработка ошибки однократного неуспешного выполнения фоновой задачи
}
await Task.Delay(5000, stoppingToken);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
// Если нужно дождаться завершения очистки, но контролировать время, то стоит предусмотреть в контракте использование CancellationToken
await someService.DoSomeCleanupAsync(cancellationToken);
return Task.CompletedTask;
}
}
Inheritance from BackgroundService
Background Service is an abstract class that implements IHostedService
handles start and stop itself by providing 1 abstract method ExecuteAsync
:
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_executingTask = ExecuteAsync(_stoppingCts.Token);
return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
return;
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose() => _stoppingCts.Cancel();
}
StartAsync
And StopAsync
can still be reloaded. Implementation of background tasks via BackgroundService
suitable for all scenarios where you do not need to block the application from starting until the operation is completed.
General implementation of a background periodic task:
public class MyHostedService : BackgroundService
{
private readonly ISomeBusinessLogicService someService;
public MyHostedService(ISomeBusinessLogicService someService)
{
this.someService = someService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Выполняем задачу пока не будет запрошена остановка приложения
while (!stoppingToken.IsCancellationRequested)
{
try
{
await someService.DoSomeWorkAsync();
}
catch (Exception ex)
{
// обработка ошибки однократного неуспешного выполнения фоновой задачи
}
await Task.Delay(5000);
}
// Если нужно дождаться завершения очистки, но контролировать время, то стоит предусмотреть в контракте использование CancellationToken
await someService.DoSomeCleanupAsync(cancellationToken);
}
}
When and how is IHostedService started
Fun fact, the correct answer is “depends”. From the .net version.
In .NET Core 2.x IHostedService
were launched after Kestrel was configured and started, that is, after the application starts listening on ports to receive requests. This means that, for example, we can get an object in the background service IServer
And IServerAddressesFeature
and be sure that when the background service starts, the list of listening addresses will already be configured. It also means that at the time of launch IHostedService
the application may already be responding to client requests, so it cannot be guaranteed that at the time the request is processed, any of IHostedService
already launched.
In .NET Core 3.0 with the transition to a new abstraction IHost
(in fact, the universal node appeared already in .net core 2.1) the behavior has changed – now Kestrel has started to run as a separate IHostedService
last after all the others IHostedService
. Actually the background services are started before the method Statup.Configure()
. Now you can ensure that all other background services are running when you start listening on ports and processing requests, and you can also not start processing requests until one of the background services has finished starting by using an override StartAsync
.

In .NET 6, things have changed a bit again. Appeared Minimal Hosting APIin which there is no additional abstraction in the form of Startup.cs, and the application is configured explicitly using a new class web application. It should be noted here that the new API is included by default in the aspnet application template, so it will be used for new projects out of the box. Everything IHostedService
in this case run when you call WebApplication.Run()
, that is, after you have configured the application and the list of listening addresses. More about this is written in issue on github.
In fact, this means that the behavior and available in IHostedService
parameters may vary depending on .net version and hosting method, and cannot rely internally on Kestrel to be already configured and running. So if a background service is running with a Kestrel config, then you need a way to wait for it to start inside IHostedService
.
Waiting for Kestrel to start inside IHostedService
Starting from version 3.0, asp.net core has a service that allows you to receive notifications that the application has completed launching and has started processing requests – this is IHostApplicationLifetime.
public interface IHostApplicationLifetime
{
CancellationToken ApplicationStarted { get; }
CancellationToken ApplicationStopping { get; }
CancellationToken ApplicationStopped { get; }
void StopApplication();
}
CancellationToken
provides a convenient mechanism for safely launching callbacks when an event occurs:
lifetime.ApplicationStarted.Register(() => DoSomeAction());
Thanks to this, we can wait for the application to start. But while waiting for the launch, we need to handle the situation when there are problems with the start – then the application will never start, and the method that is waiting for the start will not complete. To fix this, it is enough to wait not only ApplicationStarted
but also handle the event for stoppingToken
coming to ExecuteAsync
. Here’s what a complete example of a background service would look like that waits for the application to start and correctly handles startup errors:
public class MyHostedService : BackgroundService
{
private readonly ISomeBusinessLogicService someService;
private readonly IHostApplicationLifetime lifetime;
public MyHostedService(ISomeBusinessLogicService someService, IHostApplicationLifetime lifetime)
{
this.lifetime = lifetime;
this.someService = someService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!await WaitForAppStartup(lifetime, stoppingToken))
return;
// Приложение запущено и готово к обработке запросов
// Выполняем задачу пока не будет запрошена остановка приложения
while (!stoppingToken.IsCancellationRequested)
{
try
{
await someService.DoSomeWorkAsync();
}
catch (Exception ex)
{
// обработка ошибки однократного неуспешного выполнения фоновой задачи
}
await Task.Delay(5000);
}
// Если нужно дождаться завершения очистки, но контролировать время, то стоит предусмотреть в контракте использование CancellationToken
await someService.DoSomeCleanupAsync(cancellationToken);
}
static async Task<bool> WaitForAppStartup(IHostApplicationLifetime lifetime, CancellationToken stoppingToken)
{
// 👇 Создаём TaskCompletionSource для ApplicationStarted
var startedSource = new TaskCompletionSource();
using var reg1 = lifetime.ApplicationStarted.Register(() => startedSource.SetResult());
// 👇 Создаём TaskCompletionSource для stoppingToken
var cancelledSource = new TaskCompletionSource();
using var reg2 = stoppingToken.Register(() => cancelledSource.SetResult());
// Ожидаем любое из событий запуска или запроса на остановку
Task completedTask = await Task.WhenAny(startedSource.Task, cancelledSource.Task).ConfigureAwait(false);
// Если завершилась задача ApplicationStarted, возвращаем true, иначе false
return completedTask == startedSource.Task;
}
}
Third party libraries for background tasks
Another popular (57 million downloads) solution for dotnet background tasks is Hangfirea library for setting up, launching and storing background tasks with a free version for commercial use, a lot of settings and a separate task admin.

What to read about background tasks in asp.net
Materials compiled from Microsoft docs articles, Andew Lock articles, and Scott Sauber talk at Rome .NET Conference: