A long-running process alongside the web application is a first-class concept here, rather than a separate supervisor configuration and a script.
public class OrderConsumer : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var message = await _queue.ReceiveAsync(stoppingToken);
await _handler.Handle(message, stoppingToken);
}
}
}
// services.AddHostedService<OrderConsumer>();
The cancellation token threaded through every call is what makes shutdown graceful — a SIGTERM cancels it and the loop finishes the current message rather than being killed mid-write. That is the part a PHP worker builds by hand with signal handlers, and getting it right is fiddly. Running the consumer in the same process as the web application couples their scaling, which is a decision worth making deliberately rather than by default.