How to monitor your ASP.NET Core app with uptime checks and heartbeat monitoring (free)
Your ASP.NET Core API went down at 3 AM and you found out the next morning when a client called. The Kestrel process had exited silently hours earlier and no one knew.
.NET has excellent built-in tooling for health checks, but it doesn't reach out and tell you when something goes wrong — you need an external monitor for that. By the end of this article you'll have HTTP uptime monitoring, heartbeat checks for your background services, email and Slack alerts, and a public status page — all in under 30 minutes, free.
The two failure modes .NET developers miss
Endpoint outages — your /health or /api/ routes start returning 500s or the process crashes. Users hit errors. You don't know until someone tells you.
Silent background service failures — an IHostedService or Hangfire job throws an exception that gets swallowed, or your recurring task stops being scheduled entirely. No crash. No obvious log entry. The work just quietly stops happening.
Both are easy to detect with an external monitor checking from outside your infrastructure.
Step 1: Add a /health endpoint with ASP.NET Core Health Checks
ASP.NET Core has a built-in health check system since .NET Core 2.2. Add the health checks middleware to your Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")!) // optional: DB check
.AddRedis(builder.Configuration.GetConnectionString("Redis")!); // optional: cache check
var app = builder.Build();
// Map the health endpoint
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
For richer JSON output, install AspNetCore.HealthChecks.UI.Client:
dotnet add package AspNetCore.HealthChecks.UI.Client
dotnet add package AspNetCore.HealthChecks.NpgSql # if using PostgreSQL
dotnet add package AspNetCore.HealthChecks.Redis # if using Redis
The health endpoint returns 200 OK when all checks pass and 503 Service Unavailable when any check fails. That 503 is the critical signal — it gives your monitoring tool a meaningful status to alert on.
A minimal health handler with no external dependencies:
// Minimal version — no NuGet packages needed
app.MapGet("/health", () => Results.Ok(new
{
status = "ok",
timestamp = DateTime.UtcNow
}));
Step 2: Set up HTTP uptime monitoring
With your /health endpoint live, point Vigilmon at it:
- Sign up for a free account at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval to 5 minutes (free tier)
- Save
Vigilmon probes your endpoint from multiple regions every 5 minutes. The moment it gets anything other than 2xx — or hits a timeout — it opens an incident and sends you an alert.
You can stack multiple monitors for the same service:
-
https://yourdomain.com/health— deep health check including DB and cache -
https://yourdomain.com/— confirms the frontend is serving -
https://yourdomain.com/api/v1/ping— confirms your API layer is reachable
Step 3: Heartbeat monitoring for IHostedService and Hangfire
HTTP monitoring tells you when the server is down. But what about your scheduled work?
The heartbeat pattern: your background job pings a URL at the end of every successful run. If Vigilmon stops receiving that ping within the expected window, it fires an alert — even if your server is still running fine.
With IHostedService
public class DailyReportService : BackgroundService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
private readonly ILogger<DailyReportService> _logger;
public DailyReportService(
IHttpClientFactory httpClientFactory,
IConfiguration config,
ILogger<DailyReportService> logger)
{
_httpClientFactory = httpClientFactory;
_config = config;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await RunDailyReport(stoppingToken);
await PingHeartbeat(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Daily report failed — skipping heartbeat ping");
// Don't ping on failure; Vigilmon will alert after the missed interval
}
await Task.Delay(TimeSpan.FromHours(24), stoppingToken);
}
}
private async Task RunDailyReport(CancellationToken ct)
{
_logger.LogInformation("Running daily report...");
// your report logic here
}
private async Task PingHeartbeat(CancellationToken ct)
{
var url = _config["Heartbeats:DailyReport"];
if (string.IsNullOrEmpty(url)) return;
var client = _httpClientFactory.CreateClient();
try
{
await client.GetAsync(url, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Heartbeat ping failed");
}
}
}
Register it in Program.cs:
builder.Services.AddHttpClient();
builder.Services.AddHostedService<DailyReportService>();
And in appsettings.json:
{
"Heartbeats": {
"DailyReport": "https://vigilmon.online/heartbeats/your-unique-token"
}
}
With Hangfire
public class ReportJob
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
public ReportJob(IHttpClientFactory httpClientFactory, IConfiguration config)
{
_httpClientFactory = httpClientFactory;
_config = config;
}
public async Task Execute()
{
// your job logic
await ProcessReports();
// ping heartbeat on success
var url = _config["Heartbeats:DailyReport"];
if (!string.IsNullOrEmpty(url))
{
var client = _httpClientFactory.CreateClient();
await client.GetAsync(url);
}
}
}
Creating the heartbeat monitor in Vigilmon
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. every 24 hours)
- Set the grace period (how long to wait after a missed ping before alerting)
- Copy the unique ping URL it generates
- Set it as your
Heartbeats:DailyReportconfig value
Now if your job throws an unhandled exception, the await Task.Delay never fires again, or Hangfire's scheduler gets confused — Vigilmon alerts you within one missed interval.
One heartbeat per critical job
Give each important background job its own heartbeat:
{
"Heartbeats": {
"DailyReport": "https://vigilmon.online/heartbeats/token-1",
"OrderProcessing": "https://vigilmon.online/heartbeats/token-2",
"DataSync": "https://vigilmon.online/heartbeats/token-3"
}
}
Treat background jobs like endpoints — each one deserves its own uptime check.
Step 4: Alert configuration — email and Slack webhook
Email alerts are on by default in Vigilmon — you'll receive an email the moment a monitor goes down and another when it recovers. No extra configuration needed.
For Slack:
- Create an incoming webhook in your Slack workspace (Slack Apps → Incoming Webhooks)
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste the webhook URL
- Enable it on your monitors
For a custom webhook (PagerDuty, Teams, Discord, or your own endpoint):
- In Vigilmon, go to Notifications → New Channel → Webhook
- Enter your endpoint URL
- Vigilmon will POST a JSON payload on every state change
Example alert you'll receive:
🔴 ALERT: api.yourdomain.com/health is DOWN
Checked from: US-East, EU-West
Status: 503 Service Unavailable
Started: 4 minutes ago
And when it recovers:
✅ RESOLVED: api.yourdomain.com/health is back UP
Downtime: 12 minutes
Step 5: Public status page
During an incident, the first thing your users do is Google "is [your service] down?" A public status page gives them a self-service answer and reduces your support load.
In Vigilmon:
- Go to Status Pages → New Status Page
- Name it and select which monitors to display
- Share the public URL (e.g.
status.yourdomain.com)
You can embed it in your API docs or link to it from your app's error page. Users check it during incidents so your support inbox doesn't get flooded.
What you've built
In under 30 minutes:
| What | How |
|---|---|
| HTTP uptime monitoring |
/health endpoint + Vigilmon HTTP monitor |
| Dependency health checks |
AddNpgsql / AddRedis health check packages |
| Background service monitoring | Heartbeat ping at end of each IHostedService run |
| Hangfire job monitoring | Heartbeat ping at end of each Hangfire job |
| Email + Slack alerts | Vigilmon notification channels |
| Public status page | Vigilmon status page |
The full setup costs $0 on the free tier and takes less time than triaging a silent background job failure you didn't know had been failing for three days.
Next steps
- Add structured logging with Serilog alongside Vigilmon for correlated incident investigation
- Set response time thresholds in Vigilmon to catch latency regressions before they become outages
- Add a heartbeat for every
IHostedService— not just the ones that feel important. Silent failures happen to the tasks you least expect
Get started free at vigilmon.online — no credit card required, monitors running in under a minute.
Top comments (0)