How to Fix ASP.NET Core Windows Service Graceful Shutdown Issues

Make your ASP.NET Core app running as a Windows Service run its cleanup code when Windows reboots or shuts down, not only when you stop the service by hand.

By Petrus Sheya

October 11, 2026 · 4 min read

Your ASP.NET Core Windows Service skips its cleanup on reboot because it doesn't listen for the Windows shutdown signal by default, so Windows ends it without telling the host. Set CanShutdown = true on the WindowsServiceLifetime in Program.cs and give the host a ShutdownTimeout, and your ApplicationStopping code runs on reboot the same way it does when you stop the service by hand.

These steps are for .NET 8 and later, running on Windows.

How to make the service shut down gracefully on reboot

  1. Set a shutdown timeout. Tell the host how long to wait for your cleanup code before it gives up:

    var builder = WebApplication.CreateBuilder(args);
     
    builder.Services.Configure<HostOptions>(options =>
        options.ShutdownTimeout = TimeSpan.FromSeconds(15));
  2. Register the app as a Windows Service. Add the service registration and your own services:

    builder.Services.AddWindowsService(options =>
        options.ServiceName = "MyService");
     
    builder.Services.AddSingleton<IDeviceHandler, DeviceHandler>();

    AddWindowsService does the same job as builder.Host.UseWindowsService(), so you only need one of them.

  3. Build the app.

    var app = builder.Build();
  4. Get the Windows service lifetime. Resolve IHostLifetime from the container and check that it's a WindowsServiceLifetime. It only is when the app runs as a service on Windows, so wrap it in a platform check:

    using System.Runtime.InteropServices;
    using Microsoft.Extensions.Hosting.WindowsServices;
     
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    {
        var lifetime = app.Services.GetRequiredService<IHostLifetime>();
     
        if (lifetime is WindowsServiceLifetime serviceLifetime)
        {
            // step 5 goes here
        }
    }
  5. Turn on CanShutdown. Inside that if block, set both flags:

    serviceLifetime.CanStop = true;
    serviceLifetime.CanShutdown = true;

    CanShutdown is the one that matters here. With it on, Windows sends your service the shutdown signal during a reboot, and the host starts its normal stop sequence. Set it before app.RunAsync(), because the service reads these flags when it starts.

  6. Hook your cleanup into ApplicationStopping. Register a callback that closes your connections. StopAll below stands in for whatever method your own handler uses:

    var appLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
    var devices = app.Services.GetRequiredService<IDeviceHandler>();
     
    appLifetime.ApplicationStopping.Register(() => devices.StopAll());

    You can also inject IHostApplicationLifetime into DeviceHandler and register the callback in its constructor. That lets you build a linked CancellationTokenSource from ApplicationStopping and pass it to every running process.

  7. Run the app.

    await app.RunAsync();
  8. Test it with a real reboot. Publish and reinstall the service, start a process that opens a connection, then reboot Windows. After it comes back, open Event Viewer > Windows Logs > Application and look for the log entries your cleanup writes. Stopping the service from the Services tab in Task Manager doesn't test this, since that path worked all along.

Keep the cleanup quick. Windows doesn't wait forever on a reboot, so close connections in parallel where you can and stay inside the ShutdownTimeout you set.

Put the cleanup in an IHostedService instead

If you'd rather keep shutdown logic out of Program.cs, move it into a hosted service. The host calls StopAsync during the same stop sequence, and the CancellationToken it passes in fires when ShutdownTimeout runs out.

public class DeviceShutdownService : IHostedService
{
    private readonly IDeviceHandler _devices;
 
    public DeviceShutdownService(IDeviceHandler devices) => _devices = devices;
 
    public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 
    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // Close every open device connection before the process exits
        await _devices.StopAllAsync(cancellationToken);
    }
}

Register it with builder.Services.AddHostedService<DeviceShutdownService>();.

This route has the same catch as ApplicationStopping. StopAsync runs when you stop the service by hand, but on reboot it only runs once CanShutdown is set to true (step 5 above).

FAQ

Why does my cleanup run when I stop the service manually but not on reboot? Stopping a service and shutting down Windows are two different signals. Your service always accepts the stop signal, but it ignores the shutdown signal unless CanShutdown is set to true on the WindowsServiceLifetime.

Do I need an IHostedService to clean up on shutdown? No. Once CanShutdown is on, a callback registered on IHostApplicationLifetime.ApplicationStopping runs on reboot too. An IHostedService with StopAsync is just another place to put the same cleanup.

Caption Your Videos in Minutes

and keep people watching

Try Sycamore