Is there an existing issue for this?
Describe the bug
In HealthCheckPublisherHostedService.StopAsync, the timers are closed with the simple Timer.Dispose():
if (_timers != null)
{
foreach (var timer in _timers)
{
timer.Dispose();
}
_timers = null;
}
System.Threading.Timer.Dispose() does not wait for a callback that is already running. It frees the timer and returns right away. So if a timer callback is still running when StopAsync runs, StopAsync returns while that callback keeps going.
Timer has other overloads that do wait. Dispose(WaitHandle notifyObject) signals when all callbacks are done. DisposeAsync() returns a task that finishes when the callback finishes. StopAsync uses neither. So a publish that started just before the stop keeps running after the hosted service has stopped.
The docs already note this about the sample TimedHostedService ("the Timer doesn't wait for previous executions of DoWork to finish"). But here the type is internal sealed, so we cannot change the dispose call or wait for the running publish ourselves. The fix has to be in the framework.
Source (main): https://github.com/dotnet/aspnetcore/blob/main/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs
Expected Behavior
StopAsync should not finish while a timer callback is still running. A waiting dispose overload (Dispose(WaitHandle) or DisposeAsync) would make the stop reliable, so no publish runs after the service has stopped.
Steps To Reproduce
Minimal repro (public repo, Empty console host, only Hosting + HealthChecks):
https://github.com/youribroskij/healthcheck-publisher-stopasync-repro
Run dotnet run. The program:
- Starts a host with one health check and one
IHealthCheckPublisher whose PublishAsync runs for ~3s.
- Waits until a publish is in flight.
- Calls
await host.StopAsync() and records when it returns.
- Shows that
StopAsync returns while the publish is still running, and the publish finishes after the stop returned.
Sample output:
[ 207 ms] PublishAsync STARTED (will run ~3000 ms)
[ 208 ms] calling host.StopAsync()...
[ 209 ms] host.StopAsync() RETURNED
PublishRunning immediately after StopAsync returned = True
[ 3214 ms] PublishAsync FINISHED (publish finished AFTER stop)
Exceptions (if any)
No response
.NET Version
8.0.411
Anything else?
ASP.NET Core version: Microsoft.Extensions.Diagnostics.HealthChecks 8.0.0 (Microsoft.Extensions.Hosting 8.0.0), target net8.0
IDE: JetBrains Rider 2026.1.2
.NET SDK:
Version: 8.0.411
Commit: f97ff31961
Workload version: 8.0.400-manifests.ee7a832d
MSBuild version: 17.11.31+933b72e36
Runtime Environment:
OS Name: Mac OS X
OS Version: 26.5
OS Platform: Darwin
RID: osx-arm64
Base Path: /Users/youribroskij/.dotnet/sdk/8.0.411/
Host:
Version: 10.0.7
Architecture: arm64
Commit: b16286c228
.NET SDKs installed:
8.0.411, 9.0.100, 9.0.202, 9.0.205, 9.0.301, 10.0.203
Links / references:
Is there an existing issue for this?
Describe the bug
In
HealthCheckPublisherHostedService.StopAsync, the timers are closed with the simpleTimer.Dispose():System.Threading.Timer.Dispose()does not wait for a callback that is already running. It frees the timer and returns right away. So if a timer callback is still running whenStopAsyncruns,StopAsyncreturns while that callback keeps going.Timerhas other overloads that do wait.Dispose(WaitHandle notifyObject)signals when all callbacks are done.DisposeAsync()returns a task that finishes when the callback finishes.StopAsyncuses neither. So a publish that started just before the stop keeps running after the hosted service has stopped.The docs already note this about the sample TimedHostedService ("the Timer doesn't wait for previous executions of DoWork to finish"). But here the type is
internal sealed, so we cannot change the dispose call or wait for the running publish ourselves. The fix has to be in the framework.Source (main): https://github.com/dotnet/aspnetcore/blob/main/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs
Expected Behavior
StopAsyncshould not finish while a timer callback is still running. A waiting dispose overload (Dispose(WaitHandle)orDisposeAsync) would make the stop reliable, so no publish runs after the service has stopped.Steps To Reproduce
Minimal repro (public repo, Empty console host, only Hosting + HealthChecks):
https://github.com/youribroskij/healthcheck-publisher-stopasync-repro
Run
dotnet run. The program:IHealthCheckPublisherwhosePublishAsyncruns for ~3s.await host.StopAsync()and records when it returns.StopAsyncreturns while the publish is still running, and the publish finishes after the stop returned.Sample output:
Exceptions (if any)
No response
.NET Version
8.0.411
Anything else?
ASP.NET Core version: Microsoft.Extensions.Diagnostics.HealthChecks 8.0.0 (Microsoft.Extensions.Hosting 8.0.0), target net8.0
IDE: JetBrains Rider 2026.1.2
.NET SDK:
Version: 8.0.411
Commit: f97ff31961
Workload version: 8.0.400-manifests.ee7a832d
MSBuild version: 17.11.31+933b72e36
Runtime Environment:
OS Name: Mac OS X
OS Version: 26.5
OS Platform: Darwin
RID: osx-arm64
Base Path: /Users/youribroskij/.dotnet/sdk/8.0.411/
Host:
Version: 10.0.7
Architecture: arm64
Commit: b16286c228
.NET SDKs installed:
8.0.411, 9.0.100, 9.0.202, 9.0.205, 9.0.301, 10.0.203
Links / references: