.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.
📡 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.
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/liveand/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: 10Custom 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 }
)
};
}
}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]) > 0Alert Pro
14-day free trialStop 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 Support | Standout Feature | Pricing |
|---|---|---|---|
| App Insights (Azure) | Excellent | Native Azure integration, .NET SDK auto-correlation, Live Metrics | 5GB/day free + $2.30/GB |
| Datadog APM | Excellent | Continuous profiler, EF Core + SqlClient auto-trace, CLR metrics | $31/host/month |
| New Relic .NET Agent | Excellent | 100GB/month free, WCF support, distributed tracing | Free + $0.35/GB |
| Elastic APM .NET Agent | Good | ASP.NET Core + EF Core auto-instrument, Elasticsearch integration | Free tier + $16/mo |
| Better Stack | Good | Uptime monitoring + log shipping from .NET Serilog/NLog | Free + $20/mo |
| Sentry for .NET | Good | Exception tracking, ASP.NET Core middleware, source maps | Free 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
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.”
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.”
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.”
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.”
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.”
Related Guides
Best APM Tools 2026
Compare top application performance monitoring platforms.
OpenTelemetry Guide 2026
OTel collectors, exporters, and backend setup for any stack.
Kubernetes Monitoring Guide
Pod metrics, HPA alerting, and node observability.
Error Tracking Guide 2026
Sentry, Rollbar, Bugsnag — exception monitoring comparison.
Alert Pro
14-day free trialStop 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