ASP.NET Core – Control the graceful shutdown time for background services

ASP.NET Core gives you a chance to gracefully shutdown your background services (such as cleaning up and/or finishing in-progress work). By default, you’re given a grand total of 5 seconds to shutdown all background services. If you need more time than this, there are a few ways to control the graceful shutdown time, which I’ll explain in this article. Note: When I say ‘background services’, I’m specifically referring to the BackgroundService implementation of IHostedService.

Here’s how the framework handles background services during a graceful shutdown (read the code) :

  • Creates a shared cancellation token with a default timeout of 5 seconds.
  • Calls StopAsync() for each background service, passing in the shared cancellation token. This is a virtual method in BackgroundService that does the following:
    • Cancel’s the background service’s stoppingToken (the cancellation token passed into ExecuteAsync()).
    • Waits for either 1) ExecuteAsync() to complete -or- 2) the shared cancellation token to be canceled.

Note: It loops through the background services in the reverse order that they were registered.

First, you can change the shared cancellation token’s timeout by setting HostOptions.ShutdownTimeout in the initialization code. For example, this increases it to 30 seconds:

var builder = WebApplication.CreateBuilder(args);

// Omitted code for brevity

builder.Services.AddHostedService<CustomBackgroundService>();

builder.Services.Configure<HostOptions>(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

// Omitted code for brevity
Code language: C# (cs)

Second, you can override StopAsync() and control how/when the the shared cancellation token is used, effectively giving you full control over the graceful shutdown time.

public async override Task StopAsync(CancellationToken sharedShutdownToken)
{
	//Before or after stopping ExecuteAsync() - clean up / finish work / delay

	//Option 1 - Use the sharedShutdownToken
	await base.StopAsync(sharedShutdownToken);

	//Option 2 - Use your own cancellation token with a timeout
	using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
	await base.StopAsync(cts.Token);

        //Option 3 - Use an entirely different mechanism for signaling ExecuteAsync() to shutdown. 
        //You don't have to use base.StopAsync() to stop it.
}
Code language: C# (cs)

Note: To be clear, this is not a full implementation. This is just to show you how you can control the graceful shutdown time.

In previous versions of ASP.NET Core (I believe before v3.1), it checked the shared cancellation token for a timeout and broke out of the loop, potentially skipping background services. That wasn’t good. Your only option was to increase the time with HostOptions.ShutdownTime and hope it was enough. They fixed this by removing the check, which gives you control over how your background services are shutdown.

For reference – .NET source code

Here’s the .NET source code (as recent as .NET 7) that handles shutting down background services:

//Snippet from Microsoft.Extensions.Hosting.Internal.Host.StopAsync()
using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
{
	CancellationToken token = linkedCts.Token;
	// Trigger IHostApplicationLifetime.ApplicationStopping
	_applicationLifetime.StopApplication();

	IList<Exception> exceptions = new List<Exception>();
	if (_hostedServices != null) // Started?
	{
		foreach (IHostedService hostedService in _hostedServices.Reverse())
		{
			try
			{
				await hostedService.StopAsync(token).ConfigureAwait(false);
			}
			catch (Exception ex)
			{
				exceptions.Add(ex);
			}
		}
	}
	//Omitted code for brevity
Code language: C# (cs)

Here’s the BackgroundService class, showing what happens by default in StopAsync():

public abstract class BackgroundService : IHostedService, IDisposable
{
	protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

	/// <summary>
	/// Triggered when the application host is performing a graceful shutdown.
	/// </summary>
	public virtual async Task StopAsync(CancellationToken cancellationToken)
	{
		// Stop called without start
		if (_executeTask == null)
		{
			return;
		}

		try
		{
			// Signal cancellation to the executing method
			_stoppingCts.Cancel();
		}
		finally
		{
			// Wait until the task completes or the stop token triggers
			await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false);
		}

	}
	
	//Omitted code for brevity
}
Code language: C# (cs)

Be sure to read more about how to unit test async methods.

Leave a Comment