Framework Guide

C# .NET API Monitoring

Set up health check endpoints and uptime monitoring for your ASP.NET Core application. Covers built-in health checks, custom implementations, database checks, and external monitoring with UptimeSignal.

Why Monitor Your .NET API?

ASP.NET Core applications can fail in subtle ways that don't crash the process. Database connections exhaust, downstream services time out, or memory pressure causes GC pauses. External monitoring catches these before users file support tickets.

  • Database connection pool exhaustion -- All connections are in use and new requests queue indefinitely
  • GC pressure and memory leaks -- Large object heap fragmentation causes long GC pauses that freeze requests
  • Downstream service failures -- HttpClient calls to external APIs time out and cascade through your app
  • Thread pool starvation -- Blocking async-over-sync calls exhaust the thread pool, making the app unresponsive

Built-in Health Checks (Minimal API)

ASP.NET Core ships with Microsoft.Extensions.Diagnostics.HealthChecks built into the framework. No extra NuGet packages needed for basic health checks.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health");

app.MapGet("/", () => "Hello World");

app.Run();

This gives you a /health endpoint that returns 200 Healthy or 503 Unhealthy. The framework handles everything -- status codes, response text, and executing registered checks.

Built-in Health Checks (Controller-based)

If you're using the controller pattern with AddControllers(), health checks work the same way. The health check middleware is separate from MVC routing.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

Custom Health Checks with IHealthCheck

Implement IHealthCheck to create checks for anything -- external APIs, file systems, message queues, or business logic validations.

using Microsoft.Extensions.Diagnostics.HealthChecks;

public class ExternalApiHealthCheck(
    IHttpClientFactory httpClientFactory) : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var client = httpClientFactory.CreateClient();
            client.Timeout = TimeSpan.FromSeconds(5);

            var response = await client.GetAsync(
                "https://api.stripe.com/v1/health",
                cancellationToken);

            return response.IsSuccessStatusCode
                ? HealthCheckResult.Healthy("Stripe API is reachable")
                : HealthCheckResult.Degraded($"Stripe returned {response.StatusCode}");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Cannot reach Stripe API",
                exception: ex);
        }
    }
}

// Register in Program.cs
builder.Services.AddHttpClient();
builder.Services.AddHealthChecks()
    .AddCheck<ExternalApiHealthCheck>(
        "stripe_api",
        failureStatus: HealthStatus.Degraded,
        tags: ["external", "payment"]);

Database Health Checks

The community-maintained AspNetCore.HealthChecks.* packages provide ready-made checks for SQL Server, PostgreSQL, MySQL, Redis, and more.

Entity Framework Core

// Install: dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>(
        name: "database",
        failureStatus: HealthStatus.Unhealthy,
        tags: ["db", "sql"]);

SQL Server

// Install: dotnet add package AspNetCore.HealthChecks.SqlServer

builder.Services.AddHealthChecks()
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("Default")!,
        name: "sqlserver",
        tags: ["db", "sql"]);

PostgreSQL

// Install: dotnet add package AspNetCore.HealthChecks.NpgSql

builder.Services.AddHealthChecks()
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("Postgres")!,
        name: "postgresql",
        tags: ["db", "postgres"]);

Multiple Checks Together

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database", tags: ["db"])
    .AddRedis(builder.Configuration.GetConnectionString("Redis")!, tags: ["cache"])
    .AddCheck<ExternalApiHealthCheck>("stripe", tags: ["external"])
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["self"]);

Returning JSON from Health Checks

By default, the health endpoint returns plain text (Healthy/Unhealthy). For monitoring tools like UptimeSignal, plain text with a 200/503 status code works perfectly. But if you want detailed JSON output for debugging, configure a custom response writer.

using System.Text.Json;

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";

        var result = new
        {
            status = report.Status.ToString(),
            duration = report.TotalDuration.TotalMilliseconds,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                duration = e.Value.Duration.TotalMilliseconds,
                description = e.Value.Description,
                error = e.Value.Exception?.Message
            })
        };

        await context.Response.WriteAsync(
            JsonSerializer.Serialize(result, new JsonSerializerOptions
            {
                WriteIndented = true,
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            }));
    }
});

Filtering Health Checks with Tags

Use tags to create separate endpoints for liveness and readiness -- critical for Kubernetes deployments and useful for targeted monitoring.

// Liveness: is the process alive? (no dependency checks)
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("self")
});

// Readiness: can it serve traffic? (all dependency checks)
app.MapHealthChecks("/readyz", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("db") || check.Tags.Contains("cache")
});

// Full: everything (for monitoring dashboards)
app.MapHealthChecks("/health");

Health Check UI

The AspNetCore.HealthChecks.UI package provides a dashboard for visualizing health check results across multiple services.

// Install:
// dotnet add package AspNetCore.HealthChecks.UI
// dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage

builder.Services.AddHealthChecksUI(options =>
{
    options.SetEvaluationTimeInSeconds(30);
    options.MaximumHistoryEntriesPerEndpoint(50);
    options.AddHealthCheckEndpoint("API", "/health");
})
.AddInMemoryStorage();

var app = builder.Build();

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.MapHealthChecksUI(options =>
{
    options.UIPath = "/health-ui";
});

The UI is great for internal dashboards, but it only checks when someone views the page. For 24/7 monitoring and instant alerts, pair it with UptimeSignal.

Best Practices

  • Keep checks fast -- Health checks should respond in under 500ms. Set timeouts on database and HTTP calls
  • No auth on health endpoints -- Monitoring services need unauthenticated access. Use .AllowAnonymous() if you have global auth
  • Separate liveness from readiness -- A process can be alive but not ready to serve traffic (e.g., during startup migrations)
  • Don't over-check -- Only check dependencies your app truly needs. A degraded cache shouldn't make the whole app "unhealthy"
  • Use HealthStatus.Degraded -- For non-critical issues. Return Unhealthy only when the app genuinely cannot serve requests
  • Suppress health check logs -- Filter out health check requests from your logging to reduce noise

Suppress Health Check Logging

// In appsettings.json, suppress Microsoft health check logs:
{
  "Logging": {
    "LogLevel": {
      "Microsoft.Extensions.Diagnostics.HealthChecks": "Warning"
    }
  }
}

// Or filter in middleware:
app.MapHealthChecks("/health")
    .WithMetadata(new LoggingFilterAttribute());

Monitor Your .NET API with UptimeSignal

Once your health endpoint is deployed, add it to UptimeSignal for 24/7 external monitoring. UptimeSignal pings your endpoint on a schedule and alerts you via email when it fails.

  1. Deploy your health endpoint -- Make sure /health is publicly accessible and returns 200 when healthy
  2. Sign up at UptimeSignal -- Create a free account at uptimesignal.io
  3. Add your monitor -- Enter your health check URL (e.g., https://api.yourapp.com/health)
  4. Set your interval -- Free tier checks every 5 minutes. Pro tier ($15/mo) checks every minute
  5. Get alerted -- Receive email notifications when your API goes down and when it recovers

UptimeSignal checks your endpoint from outside your infrastructure, catching issues that internal monitoring misses -- DNS failures, SSL expiration, load balancer misconfigurations, and complete infrastructure outages.

Common .NET Production Issues

Thread Pool Starvation

Calling .Result or .Wait() on async methods blocks thread pool threads. Under load, all threads get consumed and the app becomes unresponsive while still "running." External health checks with timeouts catch this because the /health endpoint stops responding.

Connection Pool Exhaustion

Forgetting to dispose HttpClient or SqlConnection objects leaks connections. Use IHttpClientFactory and using statements. A database health check that times out is often the first sign of pool exhaustion.

Large Object Heap Fragmentation

Frequent allocation of large objects (>85KB) fragments the LOH, causing long GC pauses that freeze all requests. Monitor GC.GetTotalMemory() in your health check and alert when memory grows unexpectedly between requests.

Kestrel Limits and Timeouts

Default Kestrel limits may cause request rejections under load. MaxConcurrentConnections, RequestHeadersTimeout, and KeepAliveTimeout all affect availability. External monitoring detects when these limits are hit because requests start failing.

Frequently Asked Questions

What is the built-in health check framework in ASP.NET Core?
ASP.NET Core includes Microsoft.Extensions.Diagnostics.HealthChecks, a first-party health check framework. You register health checks in Program.cs with builder.Services.AddHealthChecks(), map the endpoint with app.MapHealthChecks("/health"), and the framework handles executing checks, aggregating results, and returning appropriate HTTP status codes (200 for Healthy, 503 for Unhealthy).
How do I add a database health check to my ASP.NET Core app?
Install the AspNetCore.HealthChecks.SqlServer or AspNetCore.HealthChecks.NpgSql NuGet package. Then chain .AddSqlServer(connectionString) or .AddNpgSql(connectionString) after AddHealthChecks(). For Entity Framework Core, use .AddDbContextCheck<YourDbContext>() from Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.
Should I use /health or /healthz in ASP.NET Core?
Either works. /health is more readable and commonly used in .NET projects. /healthz follows Kubernetes conventions. If you deploy to Kubernetes, use /healthz for liveness and /readyz for readiness probes. For external monitoring with UptimeSignal, the path doesn't matter -- just be consistent across your services.
How do I write a custom health check in ASP.NET Core?
Implement the IHealthCheck interface with a single CheckHealthAsync method. Return HealthCheckResult.Healthy() when everything is fine, HealthCheckResult.Degraded() for partial issues, or HealthCheckResult.Unhealthy() for failures. Register it with builder.Services.AddHealthChecks().AddCheck<YourCustomCheck>("check_name"). You can inject services via constructor injection.
How often should I monitor my .NET API health endpoint?
For production .NET APIs, check every 1-5 minutes. UptimeSignal's free tier checks every 5 minutes, which is sufficient for most applications. Pro tier supports 1-minute intervals for faster detection. ASP.NET Core health checks are lightweight and can handle frequent polling without performance impact.

Start monitoring your .NET API

Get alerted when your .NET endpoints fail. Setup takes 30 seconds.

25 monitors free. No credit card required.

More Framework Guides