IHostedService
Hosted services are classes that has implemented the IHostedService
interface and implemented the two methods it provides, StartAsync
and StopAsync
.
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Consider this example, it just has a delay to simulate some running process and then logs the name of the hosted service:
internal sealed class MainHostedService (ILogger<MainHostedService> logger) :IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
logger.LogInformation( "Hosted Service {Service} @ {Time}" , nameof(MainHostedService), DateTime.Now);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
For the asp.net framework and runtime to know about hosted services we need to register them in the DI container:
builder.Services.AddHostedService<MainHostedService>();
When we do so, at the startup of the application, ASP.NET checks whether there are any IHostedService
implementation registered or not, and if so it will run them and waits for them to complete their task. That means until all the hosted services are finished their task the application is not started.
In case of a web application, that means for instance, no endpoints is available, or no html content could be served to the clients. If we run our application, we would see that first the hosted service is running and when it is done with its task the api is now listening to specific address and port, and the application is started! Like the following image:
Now, if we add, another hosted service, the application will wait for all of them to finish their task:
This behavior helps in scenarios that we need to do some tasks before the application is ready to serve requests, a very common example is applying Entity Framework Core database migrations, for best practices on how to apply migrations using EF Core check out this post! This way, the application will run the migrations first, and then is ready to react to incoming requests.
BackgroundService
BackgroundService
is an abstract classes that has implemented IHostedService
interface and expose an ExecuteAsync
, the following shows it without the detail implementation of each method, you could find the exact implementation in here.
public abstract class BackgroundService : IHostedService, IDisposable
{
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// removed for brevity
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// removed for brevity
}
public virtual void Dispose()
{
}
}
}
For the framework to know about them, we can add them to the DI container in the exact same way as hosted services using AddHostedService
extension method. Let's remove the two hosted services that we had and instead add our first background service and run the application again:
internal sealed class MyFirstBackGroundService(ILogger<MyFirstBackGroundService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
logger.LogInformation("Background Service {Service} @ {Time}", nameof(MyFirstBackGroundService),
DateTime.Now);
}
}
}
Looking at the code and the output there are two points I would like to mention: 1) I am using a while loop inside of the background service to demonstrate a long-running task, which is different that what we have implemented for the hosted services! 2) The application, unlike the hosted service scenario, is immediately ready to serve the requests and did not wait for the background service to finish its task, which in our case, is a never ending task due mainly to the (almost) infinite
while
loop!
It does not matter how many BackgroundService
I have in my application, at the startup time ASP.NET framework is not waiting for them to finish! That means the ExecuteAsync
method is implemented in a fire-and-forget manner, you could see its implementation in here:
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Create linked token to allow cancelling executing task from provided token
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Store the task we're executing
_executeTask = ExecuteAsync(_stoppingCts.Token); // 👈🏽 here is fire-and-forget: not awaiting the task to finish
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executeTask.IsCompleted)
{
return _executeTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
Gotchas
So far so good, but what would happen if I have a mix of IHostedService
and BackgroundService
what would happen then? The answer is easy, the application will run all of them, but before being ready to serve any request it makes sure that all hosted services are finished their task! What about background services? well, it does not matter whether they are finished or not 🤷🏻♂️ and this is one of the biggest differences between IHostedService
and BackgroundService
, the latter is better suited for long-running processes and the former for short-running tasks!
One thing I have not mentioned so far is that the order in which any of the hosted services and background services are registered in the DI container matters. In the next code snippet ASP.NET runs MainHostedService
first, and then runs the AnotherHostedService
, but if we switch the order in which they are added to the Services
collection, then the order they will be running will change too:
builder.Services.AddHostedService<MainHostedService>();
builder.Services.AddHostedService<AnotherHostedService>();
info: WebApplication1.MainHostedService[0]
Hosted Service MainHostedService @ 09/13/2025 10:54:33 👈🏽
info: WebApplication1.AnotherHostedService[0]
Hosted Service AnotherHostedService @ 09/13/2025 10:54:35 👈🏽
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7167
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5062
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /Users/saeedganji/dev/trashes/WebApplication1/WebApplication1
Compared to the following:
builder.Services.AddHostedService<AnotherHostedService>();
builder.Services.AddHostedService<MainHostedService>();
info: WebApplication1.AnotherHostedService[0]
Hosted Service AnotherHostedService @ 09/13/2025 10:55:12 👈🏽
info: WebApplication1.MainHostedService[0]
Hosted Service MainHostedService @ 09/13/2025 10:55:14 👈🏽
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7167
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5062
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /Users/saeedganji/dev/trashes/WebApplication1/WebApplication1
So far so good! Now, let's consider a mixed version with some background service, after all they are also a special hosted service! This means, the order in which we are registering background services also matters, even more, you ask why?
Consider, in the example of EF Core migrations, consider we are applying migrations inside a hosted service created for that purpose, a short running task and should be done before the application being ready to serve any incoming request! Now, consider we also have a background service that is depending on some tables/columns etc in the database, and those will only exists if the migration was successful!
If we register the background service before the hosted service, that means the framework will run the background service first, NOT WAITING for its ExecuteAsync
method to be done, and then runs the hosted service which was responsible for migrations! This will lead to a runtime error in the background service which will lead to unexpected termination of the whole application!
builder.Services.AddHostedService<MyFirstBackGroundService>();
builder.Services.AddHostedService<AnotherHostedService>();
builder.Services.AddHostedService<MainHostedService>();
and check the outcome, background service already executed its operation twice before the MainHostedService
Exceptions could happen
One the major differences is how the two behave when an exceptions is happening, since the host waits for the HostedServices
to finish their job any exception happening there prevents the application from starting. The scenario is a bit different with BackgroundService:
In previous .NET versions, when an exception is thrown from a BackgroundService.ExecuteAsync(CancellationToken) override, the exception is lost and the service appears unresponsive. The host continues to run, and no message is logged. Starting in .NET 6, when an exception is thrown from a BackgroundService.ExecuteAsync(CancellationToken) override, the exception is logged to the current ILogger. By default, the host is stopped when an unhandled exception is encountered.
A recommended actions is that if you want the application not to stop when and unhandled exceptions is happening in the background services use HostOptions
to change that behavior:
builder.Host.ConfigureHostOptions(options => options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);
Conclusion
At the end, keep in mind that IHostedService
is suited for short-running tasks and it prevents the application from starting until all the registered ones are finished with their tasks. If an exception happens, the application won't start.
On the other hand, BackgroundServices
are great for long-running tasks, and they are not awaited to finish their task! You could change their behavior in case of an exceptions by setting the BackgroundServiceExceptionBehavior
property on the HostOptions
instance.
Thanks for reading this article, enjoy coding and Dametoon Garm [2]