NServiceBus doesn’t have a health-check or anyhting similar. ASP.NET also doesn’t have a warmup check, but it does have healthchecks. But it doesn’t prevent you from sending HTTP requests to it or anything. It’s just there to verify if your service is still healthy, whatever that means for your app.
It’s like a container that reports it’s healthy, but the website is still down. Or a container that reports it’s healthy, but the background service in it definitely isn’t picking up any work it as it should.
You could create something like a WarmUpService:
public class WarmUpService(WarmUpState warmUpState, ILogger<WarmUpService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Warm-up starting...");
// Simulate async warm-up work such as HTTP calls to populate caches.
// Replace these with real calls in your application.
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
logger.LogInformation("Warm-up: cache A populated");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
logger.LogInformation("Warm-up: cache B populated");
warmUpState.MarkReady();
logger.LogInformation("Warm-up complete");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
and an ASP.NET warmup healthcheck
public class WarmUpHealthCheck(WarmUpState warmUpState) : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
return Task.FromResult(warmUpState.IsReady
? HealthCheckResult.Healthy("Warm-up complete")
: HealthCheckResult.Unhealthy("Warm-up in progress"));
}
}
And here’s the state:
public class WarmUpState
{
readonly TaskCompletionSource ready = new(TaskCreationOptions.RunContinuationsAsynchronously);
public bool IsReady => ready.Task.IsCompleted;
public Task WaitUntilReady(CancellationToken cancellationToken = default)
{
return ready.Task.WaitAsync(cancellationToken);
}
public void MarkReady() => ready.TrySetResult();
}
Then in the startup have this:
var builder = WebApplication.CreateBuilder(args);
// 1. Register the shared warm-up state as a singleton
builder.Services.AddSingleton<WarmUpState>();
// 2. Register the warm-up hosted service BEFORE UseNServiceBus.
// IHostedService.StartAsync calls run sequentially in registration
// order, so WarmUpService.StartAsync will complete before NServiceBus
// starts its hosted service and begins processing messages.
builder.Services.AddHostedService<WarmUpService>();
// 3. Configure NServiceBus. Because this registers its own IHostedService
// internally, and it comes AFTER WarmUpService, message processing
// will not begin until warm-up is complete.
var endpointConfiguration = new EndpointConfiguration("HealthCheckSample");
endpointConfiguration.UseTransport<LearningTransport>();
endpointConfiguration.UseSerialization<NewtonsoftJsonSerializer>();
builder.UseNServiceBus(endpointConfiguration);
// 4. Register the health check. In Kubernetes, map the readiness probe
// to /health/ready so the gateway does not route traffic until the
// app is fully warmed up.
builder.Services
.AddHealthChecks()
.AddCheck<WarmUpHealthCheck>("warm-up");
var app = builder.Build();
// Map health check endpoints for Kubernetes probes:
// Readiness: /health/ready (includes the warm-up check)
// Liveness: /health/live (always returns healthy)
app.MapHealthChecks("/health/ready");
app.MapHealthChecks("/health/live", new()
{
Predicate = _ => false // no checks, always healthy
});
app.Run();
Now your monitoring service can verify if it’s been warmed up and NServiceBus doesn’t process messages until the warmup phase has been completed.