In addition to HTTP queries, ASP.NET Core apps frequently do background operations. These duties might include sending emails, processing queued communications, creating reports, synchronizing data, or carrying out planned repairs.
When an application closes while a background process is still active, issues might arise. A Windows service may be stopped by an administrator, a container may restart during deployment, or a hosting platform may end an application following a timeout.
The program may depart a business operation, process the same message more than time, or abandon incomplete tasks if it handles shutdown improperly.
If the application does not handle shutdown correctly, it may abandon unfinished work, process the same message more than once, or leave a business operation in an inconsistent state.
ASP.NET Core provides hosted services and cancellation tokens to support graceful shutdown. However, a reliable implementation requires more than catching an exception. Developers must decide when to stop accepting new work, how to handle active operations, and how to recover tasks that cannot finish before the process exits.
This article explains how to implement graceful shutdown in ASP.NET Core, handle cancellation correctly, protect background jobs, and test shutdown behavior before deploying an application to production.
Understanding Graceful Shutdown in ASP.NET Core
Graceful shutdown is the process of stopping an application in an orderly manner rather than terminating it immediately.
When the ASP.NET Core host receives a shutdown signal, it begins stopping the application and its hosted services. Background services receive a cancellation signal and are expected to stop their ongoing work within the available shutdown period.
A typical shutdown sequence includes the following steps:
- The application receives a shutdown signal.
- Background services stop accepting new work.
- Active operations finish or reach a recoverable state.
- Resources such as database connections and message consumers are released.
- The application exits.
The hosting environment also matters. A container platform, Windows service manager, or cloud hosting service may impose its own termination deadline. If the application exceeds that deadline, the process may be terminated before cleanup finishes.
Why Background Services Need Cancellation Support
Consider a worker that processes an order from a queue. If the application shuts down while the worker is processing the order, the operation may be interrupted.
Without a recovery strategy, the order could remain incomplete. If the queue delivers the message again, the worker might also repeat an operation that already succeeded.
Cancellation tokens allow background services to respond to shutdown requests. They do not forcibly stop arbitrary code, and they do not guarantee that a remote operation has been cancelled.
Developers must therefore combine cancellation with appropriate transaction boundaries, idempotency, and recovery mechanisms.
Implementing a BackgroundService With Cancellation Support
ASP.NET Core provides the BackgroundService class for implementing long-running background operations.
The following example shows a maintenance worker that performs a task periodically and stops when the host requests shutdown.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class MaintenanceService : BackgroundService
{
private readonly ILogger<MaintenanceService> _logger;
public MaintenanceService(
ILogger<MaintenanceService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
_logger.LogInformation(
"Maintenance service started.");
try
{
while (!stoppingToken.IsCancellationRequested)
{
await RunMaintenanceAsync(stoppingToken);
await Task.Delay(
TimeSpan.FromMinutes(1),
stoppingToken);
}
}
catch (OperationCanceledException)
when (stoppingToken.IsCancellationRequested)
{
_logger.LogInformation(
"Maintenance service is stopping.");
}
}
private async Task RunMaintenanceAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Perform maintenance using cancellation-aware operations.
await Task.CompletedTask;
}
}
Register the service in Program.cs:
builder.Services.AddHostedService<MaintenanceService>();
The worker passes the stopping token to the maintenance operation and the delay. When the host requests shutdown, the delay can be cancelled and the loop can exit.
The maintenance method is a placeholder. In a real application, pass the cancellation token to database queries, HTTP requests, or other operations that support cancellation.
Handling Cancellation Exceptions
An OperationCanceledException can indicate that an operation was cancelled intentionally. It can also occur when an operation has its own timeout or another cancellation source.
For that reason, the example handles the exception only when the host’s stopping token has been cancelled.
Avoid catching every exception and silently continuing. Unexpected failures should be logged and handled according to the worker’s recovery policy.
For example, a temporary database failure may justify a bounded retry. A programming error may require the worker to stop or the host to restart the service.
Managing Dependency Injection Scopes in Background Workers
Background services are generally long-lived, so they should not retain request-scoped dependencies indefinitely.
Consider a worker that uses Entity Framework Core to process database records. A DbContext is normally registered as a scoped service. Injecting a scoped dependency into a long-lived worker and retaining it can cause lifetime problems and unnecessary resource retention.
A common solution is to create a dependency injection scope for each unit of work.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class OrderProcessingService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderProcessingService> _logger;
public OrderProcessingService(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessingService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope =
_scopeFactory.CreateAsyncScope();
var processor = scope.ServiceProvider
.GetRequiredService<OrderProcessor>();
await processor.ProcessNextAsync(stoppingToken);
}
catch (OperationCanceledException)
when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception exception)
{
_logger.LogError(
exception,
"Order processing failed.");
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}
}
In this example, each iteration creates a new scope and resolves an OrderProcessor. Scoped dependencies are disposed when the scope ends.
The processor should perform a bounded unit of work and accept the cancellation token. If an order cannot be completed, it should leave the order in a state that can be retried safely.
The delay in the exception handler prevents an immediate retry loop. In production, consider a bounded retry policy with backoff, maximum attempt counts, and monitoring for repeated failures.
Deciding Whether Active Work Should Finish
Not every background operation should respond to shutdown in the same way.
A report-generation task may be safe to cancel and restart. A financial operation may require additional safeguards to ensure that a transaction is not repeated after an uncertain result.
Choose a shutdown policy based on the type of work being performed.
| Background operation | Recommended approach |
|---|---|
| Periodic polling | Stop the next poll and cancel the current poll when safe |
| Report generation | Cancel or save progress when supported |
| Email delivery | Preserve the message until delivery is confirmed |
| Order processing | Finish or safely abandon the current unit of work |
| Financial transactions | Use durable state, idempotency, and reconciliation |
| Large data imports | Save checkpoints and resume from a known position |
The shutdown timeout should influence how much work a service attempts to complete. If a task normally takes several minutes but the hosting platform allows only a short termination window, waiting for the entire task may not be realistic.
A better approach is to divide long-running work into smaller units. Each unit should either complete successfully or leave enough durable state for the next process instance to resume safely.
Handling Queued Messages During Shutdown
Queue consumers need special attention because a message may be in progress when the application stops.
A reliable message-processing sequence should follow these principles:
- Stop accepting new messages when shutdown begins.
- Allow active work to finish when the remaining time permits.
- Acknowledge a message only after the required business operation succeeds.
- Make interrupted work available for redelivery when appropriate.
- Ensure that redelivery does not duplicate important business effects.
The implementation depends on the messaging system. Some brokers use explicit acknowledgements, while others use visibility timeouts, transactions, or consumer offsets.
Avoid Acknowledging Messages Too Early
Suppose a worker acknowledges a message before saving an order to the database. If the application shuts down after the acknowledgement but before the database transaction commits, the queue may consider the message complete even though the order was never saved.
A safer design commits the required business state before acknowledging the message, using transaction boundaries appropriate to the database and broker.
There is still a possible failure window: the database transaction may succeed, but the process may stop before acknowledging the message. The broker can then deliver it again.
Use idempotency keys, unique constraints, deduplication records, or other suitable mechanisms to ensure that repeated delivery does not repeat the business operation.
Graceful shutdown reduces interruptions, but durable processing and idempotency are what make the system recoverable when interruptions occur.
Configuring the Host Shutdown Timeout
ASP.NET Core allows developers to configure how long the host waits for hosted services to stop.
The following example sets the host shutdown timeout to 30 seconds:
using Microsoft.Extensions.Hosting;
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
This setting controls the host’s shutdown wait period. It does not guarantee that the operating system or hosting platform will allow the process to continue running for the entire 30 seconds.
For containerized applications, the container termination grace period must also accommodate the application’s shutdown requirements.
Consider the time required to stop accepting new work, complete or checkpoint active operations, release resources, and flush essential logs.
Avoid increasing the timeout without understanding why shutdown is taking too long. If a worker regularly exceeds its deadline, reduce the size of each unit of work or implement a durable recovery mechanism.
Understanding StopAsync and Resource Cleanup
The default BackgroundService implementation is sufficient for many applications. Override StopAsync only when the service requires additional shutdown coordination.
For example, a custom worker might need to stop a message producer before disposing of a client or other resource.
When overriding StopAsync, call the base implementation and ensure that any additional cleanup respects the supplied cancellation token.
Do not wait indefinitely for an operation that ignores cancellation. If the shutdown deadline expires, the process may be terminated before cleanup finishes.
Resource cleanup should also be tied to ownership. Dispose database contexts, message consumers, timers, and other disposable resources at the appropriate lifecycle boundary.
Avoid relying on finalizers or garbage collection to perform time-sensitive shutdown work.
Testing Graceful Shutdown
Graceful shutdown should be tested while background work is active, not only when the application is idle.
Include the following scenarios in your test plan:
| Test scenario | Expected result |
|---|---|
| Application stops while the worker is idle | The worker exits cleanly |
| Shutdown occurs during active work | The operation finishes or reaches a recoverable state |
| A downstream API becomes unavailable | The worker follows its defined failure policy |
| The shutdown deadline expires | Unfinished work remains recoverable |
| A queue message is interrupted | The message can be safely redelivered |
| A database operation is interrupted | Transaction boundaries preserve data consistency |
| Cleanup fails | The failure is recorded and does not remain hidden |
| The application restarts | Pending work can resume correctly |
For queue-based applications, use an integration-test environment that reproduces the actual broker’s acknowledgement and redelivery behavior. A unit test with a mock queue cannot prove how the production broker handles an interrupted consumer.
Also inspect logs and metrics to distinguish a successful shutdown from one that exceeded its deadline or left work pending.
Common Mistakes to Avoid
Ignoring Cancellation Tokens
A worker that ignores cancellation may continue processing until the host forcibly terminates the process. Pass cancellation tokens to supported operations and check them between units of work.
Assuming Every Operation Can Finish
Some operations take longer than the available shutdown window. Use bounded work units, checkpoints, and durable recovery rather than relying on an unlimited shutdown period.
Acknowledging Messages Before Processing Completes
Premature acknowledgement can lose work if the process stops before the business operation is committed. Follow the queue’s acknowledgement semantics and make redelivery safe.
Treating Cancellation as Proof That Work Did Not Happen
A cancellation exception does not prove that a database or remote service performed no work. Reconcile uncertain operations and use idempotency where necessary.
Catching Every Exception and Continuing
Broad exception handling can hide persistent failures and create endless retry loops. Log meaningful errors and define a clear recovery policy.
Relying Only on Graceful Shutdown
A machine failure or forced termination can prevent cleanup from running. Important background work must remain recoverable even when graceful shutdown is impossible.
Advantages and Limitations
Graceful shutdown helps reduce interrupted work, improves deployment behavior, and gives background services an opportunity to release resources cleanly.
However, it cannot guarantee that every operation completes. Hosting environments impose deadlines, dependencies may fail, and processes can be terminated unexpectedly.
The strongest design combines cooperative cancellation, bounded work, durable state, idempotent processing, and explicit recovery rules. Graceful shutdown is one part of that design, not a replacement for fault tolerance.
Summary
Graceful shutdown in ASP.NET Core requires a clear plan for stopping background services, handling active operations, and recovering work that cannot finish before the process exits.
Use BackgroundService with cancellation-aware operations, create appropriate dependency injection scopes, and configure shutdown timeouts that match the hosting environment. For queue consumers, stop accepting new messages, acknowledge completed work at the correct point, and make redelivery safe.
Finally, test shutdown while real work is in progress. A reliable background service should stop cleanly when possible and recover correctly when clean shutdown is not possible.
Best ASP.NET Core 11.0 Hosting
The feature and reliability are the most important things when choosing a good ASP.NET Core 11.0 hosting. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core , their servers are optimized for PHP web applications such as the latest ASP.NET Core 11.0 version. The performance and the uptime of the ASP.NET CoreĀ hosting service are excellent, and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its data centers are equipped with top equipment like cooling system, fire detection, high-speed Internet connection, and so on. That is why HostForLIFE.eu guarantees 99.9% uptime for ASP.NET Core . And the engineers do regular maintenance and monitoring works to assure its ASP.NET CoreĀ hosting are security and always up.

