ASP.NET Core
Two setups cover most .NET services:
- An HTTP monitor on the built-in health checks endpoint for your public API or website.
- A heartbeat monitor for background services, scheduled jobs and console apps.
You create monitors in the dashboard under Monitors → New monitor. The code below only sends pings or answers checks, so it needs no API key. The examples target .NET 8 or later.
HTTP monitor: health checks endpoint
ASP.NET Core ships health checks. MapHealthChecks returns 200 when every check is healthy or degraded and 503 when one is unhealthy, which is exactly what an HTTP monitor evaluates.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();
var app = builder.Build();
app.MapHealthChecks("/healthz");
app.Run();
AddDbContextCheck comes from the Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore package:
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
Without Entity Framework, register your own check:
using Microsoft.Extensions.Diagnostics.HealthChecks;
builder.Services.AddHealthChecks()
.AddCheck("queue", () => QueueClient.IsConnected ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy("Queue disconnected"));
Then create an HTTP monitor:
| Setting | Value |
|---|---|
| URL | https://api.example.com/healthz |
| Method | GET |
| Expected status | 200 |
| Interval | Your plan's minimum |
If the app requires authentication by default, add .AllowAnonymous() to MapHealthChecks, or keep it protected and send a header from the monitor's Request headers setting. Asniher identifies itself as AsniherBot/1.0; allow that user agent if bot protection sits in front of the endpoint.
Heartbeat: report a background service
Create a heartbeat monitor, copy its ping URL and set it as an environment variable. Configuration reads environment variables by default, so ASNIHER_HEARTBEAT_URL works the same in appsettings.json, user secrets or your hosting platform. Keep it out of source control: anyone with the URL can send pings.
ASNIHER_HEARTBEAT_URL=https://app.asniher.com/api/heartbeat/<token>
Add a small client. A ping that cannot reach Asniher logs a warning and never fails your job.
public sealed class AsniherHeartbeat(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<AsniherHeartbeat> logger)
{
private readonly string? _url = configuration["ASNIHER_HEARTBEAT_URL"];
public async Task PingAsync(string signal = "", string? output = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(_url)) return;
try
{
var http = httpClientFactory.CreateClient(nameof(AsniherHeartbeat));
using var content = new StringContent(output is { Length: > 10_000 } ? output[^10_000..] : output ?? "");
using var response = await http.PostAsync(_url + signal, content, cancellationToken);
if (!response.IsSuccessStatusCode) logger.LogWarning("Asniher ping returned {Status}", (int)response.StatusCode);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
logger.LogWarning(ex, "Asniher ping failed");
}
}
}
Report each run of a BackgroundService: /start before it, success after it, /fail with the exception if it throws.
public sealed class NightlyCleanup(AsniherHeartbeat heartbeat, ILogger<NightlyCleanup> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
do
{
await heartbeat.PingAsync("/start", cancellationToken: stoppingToken);
try
{
await CleanUpAsync(stoppingToken);
await heartbeat.PingAsync(cancellationToken: stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Cleanup failed");
await heartbeat.PingAsync("/fail", ex.ToString(), stoppingToken);
}
} while (await timer.WaitForNextTickAsync(stoppingToken));
}
private static Task CleanUpAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Register both in Program.cs:
builder.Services.AddHttpClient(nameof(AsniherHeartbeat), c => c.Timeout = TimeSpan.FromSeconds(10));
builder.Services.AddSingleton<AsniherHeartbeat>();
builder.Services.AddHostedService<NightlyCleanup>();
Set the monitor to a fixed period of 1 hour and a grace period a little longer than the longest normal run. If the app stops, the service crashes or the loop hangs, the pings stop and the monitor alerts.
The exception text is sent as job output, so it shows up in the ping log and the alert. Avoid putting secrets in exception messages.
Heartbeat: console apps and Windows Task Scheduler
For a console app started by cron, systemd timers or Windows Task Scheduler, ping from the scheduler instead of the code. The exit status becomes the signal: 0 is success, anything else a failure.
Linux
0 * * * * asniher-run -u https://app.asniher.com/api/heartbeat/<token> -- dotnet /opt/reports/Reports.dll
See asniher-run for installing the wrapper.
Windows (PowerShell)
Save as run-with-asniher.ps1 and point the scheduled task at powershell.exe -File C:\jobs\run-with-asniher.ps1:
$url = $env:ASNIHER_HEARTBEAT_URL
try { Invoke-RestMethod -Method Post -Uri "$url/start" -TimeoutSec 10 | Out-Null } catch { Write-Warning $_ }
$output = & "C:\jobs\Reports.exe" 2>&1 | Out-String
$status = $LASTEXITCODE
if ($status -lt 0 -or $status -gt 255) { $status = 1 }
if ($output.Length -gt 10000) { $output = $output.Substring($output.Length - 10000) }
try { Invoke-RestMethod -Method Post -Uri "$url/$status" -Body $output -TimeoutSec 10 | Out-Null } catch { Write-Warning $_ }
exit $status
Check that it works
curl -fsS -X POST "$ASNIHER_HEARTBEAT_URL"
The response is {"ok":true} and the ping appears in the monitor's ping log within seconds. A 404 means the token is wrong or the monitor was deleted.