BackgroundService Gotcha: Synchronous Starts
This is some behavior that I’ve found surprising (and others have as well): ASP.NET Core background services are started with a synchronous call.
Specifically, the host will invoke
IHostedService.StartAsync for all its hosted services, and
BackgroundService directly invokes
ExecuteAsync before returning from
BackgroundService assumes that its derived classes will have an asynchronous
ExecuteAsync. If the
ExecuteAsync implementation is synchronous (or starts executing with a blocking call), then problems will ensue.
The resulting behavior is that the background service will start executing, but the host will be unable to finish starting up. This will block other background services from starting.
Depending on the background service implementation, this may manifest as a delay of startup or a complete block of startup. If
ExecuteAsync is synchronous, then the host cannot continue starting up until that background service has completed. If
ExecuteAsync is asynchronous but takes a long time before it yields, then the host has its startup delayed.
This problem is common in any of these conditions:
- The hosted service has a synchronous
ExecuteAsync. In this case, the host is prevented from starting until
- The hosted service reads from a queue to process messages, but the queue reading is blocking. Even if the processing is asynchronous, the host startup is blocked until the first message arrives for this service and is (asynchronously) processed.
- The hosted service is properly asynchronous, but the asynchrony is completing immediately. E.g., if it is asynchronously reading from a queue but there are many messages immediately received, then the host startup is blocked until the background service actually yields.
Since the problem is synchronous
ExecuteAsync methods (or at least
ExecuteAsync methods that do non-trivial work before they become asynchronous), the simplest solution is to ensure
ExecuteAsync is asynchronous.
I’m not a fan of using
Task.Run to wrap the body of a method (i.e., “fake asynchrony”), but since the caller requires an asynchronous implementation, I think that’s an acceptable approach in this case:
That way, any slow or blocking code early in
ExecuteAsync will not prevent the host from starting up.