Skip to main content

How to Put a Timeout on an ASP.NET Core Health Check

· 5 min read
Jagdish Kumawat
Founder @ Dewiride

ASP.NET Core health checks have no time limit by default. My /health/ready took 64 seconds to report an unreachable MySQL server, far longer than a load balancer will wait. Setting a limit takes one parameter, or a few lines if you use AddDbContextCheck.

What You Need​

The .NET 10 SDK, and a small project: dotnet new web --no-https -n WebDemo.

Way 1: AddCheck Has a Parameter​

Replace Program.cs. SlowCheck stands in for a check on a server that doesn't answer:

Program.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
.AddCheck<SlowCheck>("slow", timeout: TimeSpan.FromSeconds(2));

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

// Stands in for a check on a server that doesn't answer.
sealed class SlowCheck : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
return HealthCheckResult.Healthy();
}
}

The important line is timeout: TimeSpan.FromSeconds(2). It only works if the check passes on its cancellationToken, as this one does.

Terminal 1
dotnet run --no-launch-profile --urls http://localhost:5300
Terminal 2
curl -s -w ' %{http_code} after %{time_total}s\n' http://localhost:5300/health
Output
Unhealthy 503 after 2.069737s

Unhealthy after 2 seconds, and not 30. The log records it:

API log
fail: Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService[104]
Health check slow threw an unhandled exception after 2007.0627ms

The health endpoint answering Unhealthy with 503 after 2 seconds, for a check that would otherwise take 30, with the log showing the check ended after about 2,007 ms.

Way 2: AddDbContextCheck Needs One More Step​

AddDbContextCheck has no timeout parameter. It does create a registration, and that has a Timeout property. So set it afterwards:

Program.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;

builder.Services.AddHealthChecks()
.AddDbContextCheck<InventoryDbContext>(name: "mysql", tags: ["ready"]);

// Report a server that doesn't answer within seconds, not after every retry has timed out.
builder.Services.Configure<HealthCheckServiceOptions>(options =>
{
foreach (var registration in options.Registrations)
{
registration.Timeout = TimeSpan.FromSeconds(5);
}
});

Here's a .NET API pointed at an address that doesn't answer, with /health/ready on that timeout, and a normal endpoint, with 4 attempts at 15 seconds each:

Output
== Connect Timeout=15 (the default)
GET /health/ready: 503 after 5.096208s
GET /api/v1/categories: 503 after 64.495866s

Before this timeout, the readiness request took 64.8 seconds as well. The log says what happened:

API log
fail: Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService[103]
Health check mysql with status Unhealthy completed after 5036.081ms with message 'A task was canceled.'

MySQL being stopped, rather than unreachable, fails faster still, at about 4 seconds, because the port refuses straight away.

Choosing a Number​

  • It should be shorter than the timeout of whatever calls it. A Kubernetes probe's timeoutSeconds defaults to 1, so check yours.
  • It should be longer than the check takes on a bad day. My readiness check takes under 100 ms when all's well, so 5 seconds is generous.

One Warning for MySql.Data​

This timeout cancels a token, on a timer. With MySql.Data, if a TLS handshake is running at that moment, that ends the process. It's the same risk Connect Timeout already has, and it fires at whichever is shorter. A handshake takes milliseconds, so it takes a network stall to reach it. Run your app under something that restarts it.

Common Mistakes​

MistakeWhat happensDo this instead
A check that ignores its cancellationTokenThe timeout can't stop itPass it on
Configuring before AddHealthChecksYour code sees an empty list, so there's no timeout: my 8-second check answered Healthy after 8 secondsConfigure after
Adding a timeout to livenessIt runs no checks, so there's nothing to timeLeave it

Troubleshooting​

SymptomLikely causeFix
with message 'A task was canceled.'That's the timeoutNow find out why it's slow
with message '(null)'The check failed quickly, and kept its reasonThe fix
It still takes the full timeThe check doesn't use its tokenPass it on

Conclusion​

Health checks don't time out on their own. Use AddCheck's timeout parameter, or set Timeout on the registrations through HealthCheckServiceOptions, and keep it below what your load balancer will wait.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.