Extend Shutdown Timeout for Azure App Service when using IHostedService in ASP.NET Core - Stack Overflow

admin2025-04-29  1

I have an ASP.NET Core app with a BackgroundService (IHostedService). Behavior:

  • The IHostedService reads messages from an Azure Service Bus Queue
  • Some messages can take up to 45 seconds to process.
  • This app is hosted as an Azure App Service.

Goal: When doing a deployment, slot swap, or stopping the App Service in Azure, I want the message to finish processing (not abruptly fail) before the app is completely shut down.

What appears to happen is regardless of the code I write in Program.cs or what environment variables I set on the App Service, the App will forcefully shut down anywhere between 5-15 seconds without finishing the StopAsync() method (during ProcessMessageAsync(). Here is an example of the log stream I see:

Notice how the logging messages from StopAsync() are not logged because await _serviceBusProcessor.StopProcessingAsync() is blocking and never finishes. I expect the message to finish all 20 loops and the rest of StopAsync() to execute.

When I run the app locally in Visual Studio, the app delays shutdown until the message is finished processing with the correct outputs when I enter Ctrl + C:

My code:

Program.cs:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.ConfigureHostOptions(options => options.ShutdownTimeout = TimeSpan.FromSeconds(60));
    builder.WebHost.UseShutdownTimeout(TimeSpan.FromSeconds(60));

    builder.Services.AddControllers();
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.AddHostedService<ServiceBusHostedService>();

    var app = builder.Build();

    app.UseStaticFiles();
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.EnableTryItOutByDefault();
    });
    
    app.UseHttpsRedirection();
    app.UseAuthorization();

    app.MapControllers();

    app.Run();
}

BackgroundService:

using Azure.Identity;
using Azure.Messaging.ServiceBus;

namespace MyApp.API.ServiceBus
{
    public class ServiceBusHostedService : BackgroundService
    {
        private readonly ILogger<ServiceBusHostedService> _logger;
        private readonly ServiceBusClient _serviceBusClient;
        private readonly ServiceBusProcessor _serviceBusProcessor;

        public ServiceBusHostedService(
            ILogger<ServiceBusHostedService> logger,
            ServiceBusConfiguration serviceBusConfiguration)
        {
            _logger = logger;
            
            _serviceBusClient = new ServiceBusClient(serviceBusConfiguration.Namespace, new DefaultAzureCredential());

            _serviceBusProcessor = _serviceBusClient.CreateProcessor(serviceBusConfiguration.WorkQueueName, new ServiceBusProcessorOptions
            {
                MaxConcurrentCalls = serviceBusConfiguration.NumberOfThreads,
                AutoCompleteMessages = false,
            });
        }


        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("ServiceBusHostedService started");

            _serviceBusProcessor.ProcessMessageAsync += ProcessMessageAsync;
            _serviceBusProcessor.ProcessErrorAsync += ProcessErrorAsync;

            await _serviceBusProcessor.StartProcessingAsync(stoppingToken);
            await Task.Delay(Timeout.Infinite, stoppingToken);
        }


        private async Task ProcessMessageAsync(ProcessMessageEventArgs args)
        {
            var message = args.Message;
            var sequenceNumber = message.SequenceNumber.ToString();

            try
            {   
                var messageTypeExists = message.ApplicationProperties.TryGetValue("MessageType", out var messageType);

                // Let's make this take about 20 seconds to process
                for (var i = 0; i < 20; i++)
                {
                    _logger.LogInformation($"Sleeping... i = {i}");
                    Thread.Sleep(1000);
                }

                _logger.LogWarning(message.Body.ToString());

                await args.CompleteMessageAsync(message);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"Error while processing message with sequence number {sequenceNumber} for {args.FullyQualifiedNamespace} and entity path {args.EntityPath}");

                try
                {
                    await args.AbandonMessageAsync(args.Message);
                }
                catch (Exception abandonEx)
                {
                    _logger.LogCritical(abandonEx, "Failed to abandon message");
                }
            }
        }


        private Task ProcessErrorAsync(ProcessErrorEventArgs args)
        {
            _logger.LogError($"Error in Service Bus Processor for {args.FullyQualifiedNamespace} at {args.ErrorSource} for entity path {args.EntityPath}: {args.Exception}");
            return Task.CompletedTask;
        }


        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation($"{nameof(ServiceBusHostedService)} is stopping");

            await _serviceBusProcessor.StopProcessingAsync();

            // Wait for ongoing message processing to complete.
            while (_serviceBusProcessor.IsProcessing)
            {
                _logger.LogInformation("Waiting for ongoing message processing to complete..");
                if (cancellationToken.IsCancellationRequested)
                {
                    _logger.LogWarning("Shutdown was forcibly canceled");
                    break;
                }
                
                await Task.Delay(1000, cancellationToken);
            }

            _logger.LogInformation("All message processing completed. Cleaning up resources");

            // Dispose of resources.
            await _serviceBusProcessor.DisposeAsync();
            await _serviceBusClient.DisposeAsync();

            await base.StopAsync(cancellationToken);

            _logger.LogInformation($"{nameof(ServiceBusHostedService)} stopped gracefully");
        }
    }
}

I've also tried setting the following environment variables on the App Service, neither of which had any impact:

  • ASPNETCORE_SHUTDOWNTIMEOUTSECONDS = 60
  • WEBSITE_WAIT_FOR_SHUTDOWN = 60

To summarize, how can I get the Azure App Service to terminate gracefully with the extended timeout that I am able to achieve when running the app locally from Visual Studio?

This is the only helpful link I could find in any Microsoft doc, but perhaps it doesn't apply to Azure App Services: .0

Any help would be much appreciated!

I have an ASP.NET Core app with a BackgroundService (IHostedService). Behavior:

  • The IHostedService reads messages from an Azure Service Bus Queue
  • Some messages can take up to 45 seconds to process.
  • This app is hosted as an Azure App Service.

Goal: When doing a deployment, slot swap, or stopping the App Service in Azure, I want the message to finish processing (not abruptly fail) before the app is completely shut down.

What appears to happen is regardless of the code I write in Program.cs or what environment variables I set on the App Service, the App will forcefully shut down anywhere between 5-15 seconds without finishing the StopAsync() method (during ProcessMessageAsync(). Here is an example of the log stream I see:

Notice how the logging messages from StopAsync() are not logged because await _serviceBusProcessor.StopProcessingAsync() is blocking and never finishes. I expect the message to finish all 20 loops and the rest of StopAsync() to execute.

When I run the app locally in Visual Studio, the app delays shutdown until the message is finished processing with the correct outputs when I enter Ctrl + C:

My code:

Program.cs:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.ConfigureHostOptions(options => options.ShutdownTimeout = TimeSpan.FromSeconds(60));
    builder.WebHost.UseShutdownTimeout(TimeSpan.FromSeconds(60));

    builder.Services.AddControllers();
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.AddHostedService<ServiceBusHostedService>();

    var app = builder.Build();

    app.UseStaticFiles();
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.EnableTryItOutByDefault();
    });
    
    app.UseHttpsRedirection();
    app.UseAuthorization();

    app.MapControllers();

    app.Run();
}

BackgroundService:

using Azure.Identity;
using Azure.Messaging.ServiceBus;

namespace MyApp.API.ServiceBus
{
    public class ServiceBusHostedService : BackgroundService
    {
        private readonly ILogger<ServiceBusHostedService> _logger;
        private readonly ServiceBusClient _serviceBusClient;
        private readonly ServiceBusProcessor _serviceBusProcessor;

        public ServiceBusHostedService(
            ILogger<ServiceBusHostedService> logger,
            ServiceBusConfiguration serviceBusConfiguration)
        {
            _logger = logger;
            
            _serviceBusClient = new ServiceBusClient(serviceBusConfiguration.Namespace, new DefaultAzureCredential());

            _serviceBusProcessor = _serviceBusClient.CreateProcessor(serviceBusConfiguration.WorkQueueName, new ServiceBusProcessorOptions
            {
                MaxConcurrentCalls = serviceBusConfiguration.NumberOfThreads,
                AutoCompleteMessages = false,
            });
        }


        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("ServiceBusHostedService started");

            _serviceBusProcessor.ProcessMessageAsync += ProcessMessageAsync;
            _serviceBusProcessor.ProcessErrorAsync += ProcessErrorAsync;

            await _serviceBusProcessor.StartProcessingAsync(stoppingToken);
            await Task.Delay(Timeout.Infinite, stoppingToken);
        }


        private async Task ProcessMessageAsync(ProcessMessageEventArgs args)
        {
            var message = args.Message;
            var sequenceNumber = message.SequenceNumber.ToString();

            try
            {   
                var messageTypeExists = message.ApplicationProperties.TryGetValue("MessageType", out var messageType);

                // Let's make this take about 20 seconds to process
                for (var i = 0; i < 20; i++)
                {
                    _logger.LogInformation($"Sleeping... i = {i}");
                    Thread.Sleep(1000);
                }

                _logger.LogWarning(message.Body.ToString());

                await args.CompleteMessageAsync(message);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"Error while processing message with sequence number {sequenceNumber} for {args.FullyQualifiedNamespace} and entity path {args.EntityPath}");

                try
                {
                    await args.AbandonMessageAsync(args.Message);
                }
                catch (Exception abandonEx)
                {
                    _logger.LogCritical(abandonEx, "Failed to abandon message");
                }
            }
        }


        private Task ProcessErrorAsync(ProcessErrorEventArgs args)
        {
            _logger.LogError($"Error in Service Bus Processor for {args.FullyQualifiedNamespace} at {args.ErrorSource} for entity path {args.EntityPath}: {args.Exception}");
            return Task.CompletedTask;
        }


        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation($"{nameof(ServiceBusHostedService)} is stopping");

            await _serviceBusProcessor.StopProcessingAsync();

            // Wait for ongoing message processing to complete.
            while (_serviceBusProcessor.IsProcessing)
            {
                _logger.LogInformation("Waiting for ongoing message processing to complete..");
                if (cancellationToken.IsCancellationRequested)
                {
                    _logger.LogWarning("Shutdown was forcibly canceled");
                    break;
                }
                
                await Task.Delay(1000, cancellationToken);
            }

            _logger.LogInformation("All message processing completed. Cleaning up resources");

            // Dispose of resources.
            await _serviceBusProcessor.DisposeAsync();
            await _serviceBusClient.DisposeAsync();

            await base.StopAsync(cancellationToken);

            _logger.LogInformation($"{nameof(ServiceBusHostedService)} stopped gracefully");
        }
    }
}

I've also tried setting the following environment variables on the App Service, neither of which had any impact:

  • ASPNETCORE_SHUTDOWNTIMEOUTSECONDS = 60
  • WEBSITE_WAIT_FOR_SHUTDOWN = 60

To summarize, how can I get the Azure App Service to terminate gracefully with the extended timeout that I am able to achieve when running the app locally from Visual Studio?

This is the only helpful link I could find in any Microsoft doc, but perhaps it doesn't apply to Azure App Services: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-8.0

Any help would be much appreciated!

Share Improve this question edited Jan 8 at 14:53 chrisbuttacavoli asked Jan 7 at 1:04 chrisbuttacavolichrisbuttacavoli 3571 gold badge4 silver badges12 bronze badges 8
  • Please share the error message. – Harshitha Commented Jan 7 at 4:28
  • This question is similar to: Graceful shutdown of IHostedService / BackgroundService. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. – D A Commented Jan 7 at 7:21
  • In my opinion we can not adjust the default graceful shutdown period. but you could try to follow this suggestion as mentioned here stackoverflow.com/questions/71653101/… – Jalpa Panchal Commented Jan 7 at 8:23
  • could you check if the w3wp.exe whether it is closed immediately on app close or not – Jalpa Panchal Commented Jan 7 at 9:35
  • I have updated this Question to compare console outputs of running the app locally vs in Azure. This works fine locally but not hosted in Azure. The other thread suggested does not apply to Azure App Services, which is where the problem resides. I looked at Kudu Process Explorer to see that w3wp.exe is closed immediately when the log stream ends. – chrisbuttacavoli Commented Jan 7 at 15:02
 |  Show 3 more comments

1 Answer 1

Reset to default 0

There are a number of restrictions and limitations on azure app service. Adjusting the graceful shutdown period is not supported on multi-tenant app services (i.e. azure app service) to configure the shutdown period.

More details in this link:

https://learn.microsoft.com/en-us/answers/questions/1125435/extend-timeout-for-graceful-shutdown-in-azure-app

In terms of multi tenant app service:

转载请注明原文地址:http://anycun.com/QandA/1745935805a91347.html