.NET / C#ASP.NET Core2026 Guide

.NET Monitoring Guide: ASP.NET Core, OpenTelemetry & Application Observability (2026)

.NET's CLR runtime, thread pool model, and generational garbage collector create specific monitoring requirements. This guide covers how to instrument ASP.NET Core apps with OpenTelemetry, expose Prometheus metrics, set up health checks for Kubernetes, monitor CLR memory and GC pressure, and choose the right .NET APM tool.

Updated April 202613 min read.NET / ASP.NET Core / C#
Staff Pick

📡 Monitor your APIs — know when they go down before your users do

Better Stack checks uptime every 30 seconds with instant Slack, email & SMS alerts. Free tier available.

Start Free →

Affiliate link — we may earn a commission at no extra cost to you

TL;DR — .NET Monitoring Checklist

  • ✅ Add ASP.NET Core health checks at /health/live and /health/ready
  • ✅ Instrument with OpenTelemetry — auto-covers HTTP, EF Core, SQL, HttpClient
  • ✅ Track CLR GC metrics — Gen2 collections and heap size indicate memory pressure
  • ✅ Monitor thread pool queue depth — starvation causes request timeouts silently
  • ✅ Use prometheus-net for Prometheus scraping from .NET apps
  • ✅ Add Sentry or Application Insights for exception tracking with stack traces

.NET-Specific Monitoring Considerations

.NET's runtime (CLR) handles memory, threading, and JIT compilation in ways that directly affect production observability:

Generational garbage collection

The CLR's GC has three generations: Gen0 (short-lived, cheapest), Gen1 (medium), and Gen2 (long-lived, most expensive). A high Gen2 collection rate signals memory pressure from long-lived objects — usually static caches, event handler leaks, or string interning abuse. Gen2 collections cause stop-the-world pauses that directly impact p99 latency. Monitor dotnet_gc_collections_total by generation.

Thread pool model

ASP.NET Core uses the .NET thread pool for request processing. Thread pool starvation — where all worker threads are blocked waiting on sync-over-async code — causes request queuing without obvious CPU or memory signals. The symptom is requests timing out while CPU is idle. Monitor thread pool queue depth (threadpool-queue-length counter) and prefer async/await patterns throughout.

Large Object Heap (LOH)

.NET allocates objects >85KB on the Large Object Heap, which is only collected during Gen2 GCs and is never compacted by default. Repeated LOH allocations cause fragmentation and memory growth. Common culprits: large byte arrays for file uploads, big string concatenations, Bitmap objects. Use ArrayPool<T> to rent and return large arrays instead of allocating fresh ones.

ASP.NET Core Health Checks

ASP.NET Core has first-class health check support. Configure liveness, readiness, and startup probes for Kubernetes:

// Program.cs — Health check configuration
builder.Services.AddHealthChecks()
    // Database connectivity
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection"),
        name: "sql-server",
        tags: ["ready", "db"]
    )
    // Redis cache
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:ConnectionString"],
        name: "redis",
        tags: ["ready", "cache"]
    )
    // External dependency
    .AddUrlGroup(
        uri: new Uri("https://api.payment-provider.com/health"),
        name: "payment-api",
        tags: ["ready", "external"]
    )
    // Custom check
    .AddCheck<QueueDepthHealthCheck>("message-queue", tags: ["ready"]);

// Separate endpoints for K8s probes
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    // Liveness: only process health (no dependency checks)
    Predicate = _ => false,
    ResponseWriter = HealthCheckResponseWriter.WriteResponse
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    // Readiness: all checks with "ready" tag
    Predicate = c => c.Tags.Contains("ready"),
    ResponseWriter = HealthCheckResponseWriter.WriteResponse
});

// Kubernetes deployment.yaml
// livenessProbe:
//   httpGet:
//     path: /health/live
//     port: 8080
//   initialDelaySeconds: 10
//   periodSeconds: 30
// readinessProbe:
//   httpGet:
//     path: /health/ready
//     port: 8080
//   initialDelaySeconds: 5
//   periodSeconds: 10

Custom Health Check Example

// Custom health check for queue depth
public class QueueDepthHealthCheck : IHealthCheck
{
    private readonly IMessageQueue _queue;

    public QueueDepthHealthCheck(IMessageQueue queue) => _queue = queue;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var depth = await _queue.GetDepthAsync();

        return depth switch
        {
            < 1000 => HealthCheckResult.Healthy(
                $"Queue depth: {depth}",
                data: new Dictionary<string, object> { ["depth"] = depth }
            ),
            < 5000 => HealthCheckResult.Degraded(
                $"Queue depth elevated: {depth}",
                data: new Dictionary<string, object> { ["depth"] = depth }
            ),
            _ => HealthCheckResult.Unhealthy(
                $"Queue depth critical: {depth}",
                data: new Dictionary<string, object> { ["depth"] = depth }
            )
        };
    }
}
📡
Recommended

Monitor your ASP.NET Core endpoints with Better Stack

Better Stack runs synthetic checks on your .NET APIs from 30+ global locations — with on-call alerting and status pages.

Try Better Stack Free →

OpenTelemetry for .NET

.NET 8+ has native OpenTelemetry support via System.Diagnostics.ActivitySource. The OpenTelemetry.Extensions.Hosting package wires everything up in DI:

// Install NuGet packages:
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Instrumentation.Http
// dotnet add package OpenTelemetry.Instrumentation.SqlClient
// dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore
// dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
// dotnet add package OpenTelemetry.Instrumentation.Runtime

// Program.cs
builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService(
        serviceName: "my-aspnetcore-api",
        serviceVersion: "2.1.0"
    ))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(o => {
            o.RecordException = true;
            o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
        })
        .AddHttpClientInstrumentation()
        .AddSqlClientInstrumentation(o => o.SetDbStatementForText = true)
        .AddEntityFrameworkCoreInstrumentation()
        .AddOtlpExporter(o => o.Endpoint = new Uri("http://localhost:4317"))
    )
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()  // CLR GC, thread pool, heap
        .AddPrometheusExporter()      // /metrics endpoint
    );

// Custom trace spans in business logic
public class OrderService
{
    private static readonly ActivitySource Source =
        new("MyApp.OrderService", "1.0.0");

    public async Task<Order> ProcessOrderAsync(OrderRequest request)
    {
        using var activity = Source.StartActivity("ProcessOrder");
        activity?.SetTag("order.value", request.TotalAmount);
        activity?.SetTag("customer.tier", request.CustomerTier);

        try
        {
            var result = await _paymentService.ChargeAsync(request);
            activity?.SetTag("payment.method", result.Method);
            return result;
        }
        catch (PaymentDeclinedException ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

Prometheus Metrics with prometheus-net

prometheus-net is the standard library for exposing Prometheus metrics from .NET. It includes ASP.NET Core middleware for automatic HTTP metrics:

// dotnet add package prometheus-net.AspNetCore

// Program.cs
builder.Services.AddHealthChecks(); // Required for health check metrics

app.UseRouting();
app.UseHttpMetrics(options => {
    // Reduce cardinality — don't create metric per URL
    options.ReduceStatusCodeCardinality();
    options.AddCustomLabel("version", ctx =>
        ctx.GetRouteValue("version")?.ToString() ?? "");
});

app.MapMetrics("/metrics"); // Prometheus scrape endpoint
app.MapHealthChecks("/health");

// Custom business metrics
public class OrderMetrics
{
    private static readonly Counter OrdersTotal = Metrics.CreateCounter(
        "orders_total",
        "Total orders processed",
        new CounterConfiguration
        {
            LabelNames = ["status", "payment_method"]
        }
    );

    private static readonly Histogram OrderAmountDollars = Metrics.CreateHistogram(
        "order_amount_dollars",
        "Order value in dollars",
        new HistogramConfiguration
        {
            Buckets = [10, 50, 100, 500, 1000, 5000]
        }
    );

    private static readonly Gauge ActiveCheckouts = Metrics.CreateGauge(
        "active_checkouts",
        "Number of checkouts in progress"
    );

    public void RecordOrder(string status, string paymentMethod, double amount)
    {
        OrdersTotal.WithLabels(status, paymentMethod).Inc();
        OrderAmountDollars.Observe(amount);
    }
}

// Key alert rules (Prometheus/Grafana)
// alert: HighDotNetGcPauseRatio
//   expr: dotnet_gc_pause_ratio > 0.1  # >10% of time in GC
// alert: ThreadPoolStarvation
//   expr: dotnet_threadpool_queue_length > 100
// alert: LargeObjectHeapGrowth
//   expr: rate(dotnet_gc_heap_size_bytes{generation="loh"}[5m]) > 0

Alert Pro

14-day free trial

Stop checking — get alerted instantly

Next time your .NET and ASP.NET Core applications goes down, you'll know in under 60 seconds — not when your users start complaining.

  • Email alerts for your .NET and ASP.NET Core applications + 9 more APIs
  • $0 due today for trial
  • Cancel anytime — $9/mo after trial

CLR Memory & GC Monitoring

Use dotnet-counters for live memory inspection and dotnet-gcdump for heap snapshots:

# Live CLR counters (attach to running process)
dotnet-counters monitor --process-id 12345 \
  --counters System.Runtime

# Key counters to watch:
# gc-heap-size         — managed heap total (MB)
# gen-0-size / gen-1-size / gen-2-size — per-generation
# loh-size             — Large Object Heap size
# gen-0-gc-count / gen-1-gc-count / gen-2-gc-count
# gc-fragmentation     — heap fragmentation %
# threadpool-queue-length
# active-timer-count
# working-set          — total process memory (MB)

# Capture GC heap dump (analyze with PerfView or VS)
dotnet-gcdump collect --process-id 12345 --output heap.gcdump

# Common memory leak patterns in .NET:
# 1. Event handler leaks — += without -= on long-lived publishers
# 2. Static dictionary growth — add keys, never remove
# 3. IDisposable not disposed — HttpClient, DbContext, streams
# 4. Closures capturing large objects
# 5. String.Intern abuse — unique strings fill intern pool forever

# Suppress LOH pressure with ArrayPool
// Instead of:
byte[] buffer = new byte[1_000_000];

// Use:
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1_000_000);
try { /* use buffer */ }
finally { pool.Return(buffer); }

.NET APM Tools Comparison

Tool.NET SupportStandout FeaturePricing
App Insights (Azure)ExcellentNative Azure integration, .NET SDK auto-correlation, Live Metrics5GB/day free + $2.30/GB
Datadog APMExcellentContinuous profiler, EF Core + SqlClient auto-trace, CLR metrics$31/host/month
New Relic .NET AgentExcellent100GB/month free, WCF support, distributed tracingFree + $0.35/GB
Elastic APM .NET AgentGoodASP.NET Core + EF Core auto-instrument, Elasticsearch integrationFree tier + $16/mo
Better StackGoodUptime monitoring + log shipping from .NET Serilog/NLogFree + $20/mo
Sentry for .NETGoodException tracking, ASP.NET Core middleware, source mapsFree 5K errors/mo + $26/mo

FAQ

What metrics should I monitor in an ASP.NET Core application?

Key metrics: request rate, latency (p50/p95/p99), error rate, CLR thread pool queue depth, GC collection counts by generation, heap memory (gc-heap-size), database connection pool utilization, and HTTP client duration for external dependencies. Start with the built-in /health endpoint and OpenTelemetry auto-instrumentation — they cover most cases with minimal custom code.

How do I add OpenTelemetry to an ASP.NET Core application?

Install OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, and OpenTelemetry.Exporter.OpenTelemetryProtocol. In Program.cs, use builder.Services.AddOpenTelemetry().WithTracing(b => b.AddAspNetCoreInstrumentation().AddOtlpExporter()).WithMetrics(b => b.AddAspNetCoreInstrumentation().AddRuntimeInstrumentation().AddPrometheusExporter()). This auto-instruments HTTP requests, EF Core, SQL, and outbound HTTP with no code changes to business logic.

How do I set up health checks in ASP.NET Core?

Use builder.Services.AddHealthChecks() with .AddSqlServer(), .AddRedis(), and .AddUrlGroup() for dependencies. Map separate endpoints: /health/live for liveness (process health only) and /health/ready for readiness (all dependencies). Kubernetes uses both — liveness restarts crashed pods, readiness removes pods from load balancer until ready.

How do I monitor .NET memory and GC in production?

Use OpenTelemetry Runtime Instrumentation for automatic CLR metrics or dotnet-counters for live monitoring. Key metrics: dotnet_gc_collections_total (by generation), gc-heap-size, loh-size, and gc-fragmentation. High Gen2 collections signal long-lived object accumulation. Use dotnet-gcdump to capture heap snapshots for investigation with PerfView.

What is the best APM tool for .NET applications?

Azure Application Insights for Azure-hosted apps (native integration, Live Metrics Stream). Datadog APM for the most complete .NET profiling with CLR metrics and EF Core tracing. New Relic for 100GB/month free tier. Elastic APM for ELK stack users. For open-source stacks: prometheus-net + Grafana covers metrics, OpenTelemetry + Jaeger covers traces.

🛠 Tools We Use & Recommend

Tested across our own infrastructure monitoring 200+ APIs daily

Better StackBest for API Teams

Uptime Monitoring & Incident Management

Used by 100,000+ websites

Monitors your APIs every 30 seconds. Instant alerts via Slack, email, SMS, and phone calls when something goes down.

We use Better Stack to monitor every API on this site. It caught 23 outages last month before users reported them.

Free tier · Paid from $24/moStart Free Monitoring
1PasswordBest for Credential Security

Secrets Management & Developer Security

Trusted by 150,000+ businesses

Manage API keys, database passwords, and service tokens with CLI integration and automatic rotation.

After covering dozens of outages caused by leaked credentials, we recommend every team use a secrets manager.

OpteryBest for Privacy

Automated Personal Data Removal

Removes data from 350+ brokers

Removes your personal data from 350+ data broker sites. Protects against phishing and social engineering attacks.

Service outages sometimes involve data breaches. Optery keeps your personal info off the sites attackers use first.

From $9.99/moFree Privacy Scan
ElevenLabsBest for AI Voice

AI Voice & Audio Generation

Used by 1M+ developers

Text-to-speech, voice cloning, and audio AI for developers. Build voice features into your apps with a simple API.

The best AI voice API we've tested — natural-sounding speech with low latency. Essential for any app adding voice features.

Free tier · Paid from $5/moTry ElevenLabs Free
SEMrushBest for SEO

SEO & Site Performance Monitoring

Used by 10M+ marketers

Track your site health, uptime, search rankings, and competitor movements from one dashboard.

We use SEMrush to track how our API status pages rank and catch site health issues early.

From $129.95/moTry SEMrush Free
View full comparison & more tools →Affiliate links — we earn a commission at no extra cost to you

Related Guides

Alert Pro

14-day free trial

Stop checking — get alerted instantly

Next time your .NET and ASP.NET Core applications goes down, you'll know in under 60 seconds — not when your users start complaining.

  • Email alerts for your .NET and ASP.NET Core applications + 9 more APIs
  • $0 due today for trial
  • Cancel anytime — $9/mo after trial