.NET performance monitoring is the practice of measuring how an application behaves for users and then using runtime, request, dependency, and code-level evidence to explain changes. For an ASP.NET Core service, start with request rate, failures, and latency; then correlate runtime metrics, distributed traces, and structured logs before collecting deeper diagnostics.
This is application performance monitoring, not network performance monitoring. Network latency, packet loss, DNS, and connection failures may affect a .NET service, but they need network evidence. A slow request alone does not prove the network is responsible.
Build monitoring around observable layers
A useful setup separates six layers instead of placing every measurement on one dashboard:
- User journey: Can the user complete sign-in, checkout, search, or another critical operation?
- HTTP service: What are the request rate, duration distribution, active requests, response status, and route?
- Dependencies: How long do database, cache, queue, DNS, and outbound HTTP operations take, and how do they fail?
- .NET runtime: What are CPU time, working set, allocation, garbage collection, lock contention, exceptions, and ThreadPool behavior?
- Application events: What operation ran, which release handled it, and what safe context explains the result?
- Host and network: Are resource limits, container throttling, name resolution, connections, or the network path contributing?
Each layer answers a different question. Metrics reveal that behavior changed across a population. Traces localize time and errors along one distributed operation. Logs describe discrete application events. Runtime diagnostics inspect what one process was doing. None is a substitute for the others.
Microsoft's .NET diagnostics overview makes the same boundary explicit: metrics are numerical measurements suited to continuous health monitoring, distributed traces correlate work across processes, and logs record application events. Specialized diagnostic tools come later when normal observability cannot explain the problem.
Establish a trustworthy ASP.NET Core baseline
Record a normal range under representative traffic before setting alerts. A baseline must identify the service, environment, version, instance, route template, and observation window. A single global average can hide one slow route, region, tenant class, or release.
Start with request and dependency symptoms
For each critical route or operation, collect:
- request count and active requests;
- a request-duration histogram that can produce useful percentiles;
- failures divided by requests at the same measurement boundary;
- response status or error class without unbounded values;
- dependency call duration, timeout, and failure class;
- deployment or service-version markers.
ASP.NET Core exposes built-in metrics, including http.server.request.duration for inbound request duration and instruments for active requests and Kestrel connections. The current ASP.NET Core metrics documentation shows that request metrics can include matched route and response status. Consult the reference for the runtime version actually deployed: available instruments, names, and OpenTelemetry behavior change between framework releases.
Use route templates such as /orders/{id}, not raw paths containing order IDs. Avoid user IDs, request IDs, exception text, URLs with query strings, or other high-cardinality values as metric tags. They multiply time series and may expose private data. Keep unique identifiers in protected traces or logs where retention and access can be controlled.
Watch runtime pressure without diagnosing from one counter
A practical runtime view includes:
- process CPU time and working set;
- managed heap size, allocation volume, collection counts, and GC pause time;
- ThreadPool thread count, queue length, and completed work;
- monitor lock contention;
- exception rate, interpreted with application failures;
- Kestrel connection and request queue signals where the deployed runtime provides them.
On .NET 9 and later, current dotnet-counters output includes instruments such as dotnet.process.cpu.time, dotnet.process.memory.working_set, dotnet.gc.pause.time, and dotnet.thread_pool.queue.length. Apps on .NET 8 or earlier fall back to older System.Runtime EventCounters with different labels and presentation. Microsoft's counter reference and runtime metrics reference should therefore be matched to the app's runtime rather than copied from a dashboard built for another version.
Interpret trends together. A growing ThreadPool queue with rising thread count and low CPU can support a blocking or starvation hypothesis. A high allocation rate alone does not prove a memory leak. Working set, managed heap, GC activity, traffic, container limits, and sustained growth tell a more complete story.
Make application logs useful for correlation
Use Microsoft.Extensions.Logging.ILogger structured message templates instead of interpolating a finished string. Keep the event name or ID stable and record only fields that help an investigation:
logger.LogWarning(
new EventId(4102, "DependencyTimeout"),
"Dependency {Dependency} timed out during {Operation} after {ElapsedMs} ms",
"inventory-api",
"reserve-stock",
elapsed.TotalMilliseconds);
The sink or console formatter determines the final representation. Verify that the stored record preserves the event ID, category, level, message template or rendered message, service, environment, version, and timestamp. Add trace and span identifiers only when a real activity context exists. A trace ID correlates evidence; it is not an authorization decision and should not contain user data.
Do not log authorization headers, cookies, passwords, tokens, connection strings, raw request bodies, or unrestricted exception data. Prefer an allowlist of operational fields. Sanitize untrusted text so line breaks and delimiters cannot forge extra log entries, and define access, retention, and deletion before increasing log detail.
Use OpenTelemetry without blurring signal boundaries
OpenTelemetry provides APIs, SDKs, instrumentation libraries, and exporters for telemetry. It does not make every library observable automatically, and it does not store, search, visualize, or alert on data by itself. A backend and an explicit export path are still required.
The current OpenTelemetry .NET documentation marks its traces, metrics, and logs components as stable. The separate .NET automatic-instrumentation distribution is still described as beta in the current instrumentation guide. Record that distinction in the deployment decision.
For an ASP.NET Core application:
- use framework or supported instrumentation for inbound HTTP and common dependencies;
- define resource identity such as stable service name, environment, and version;
- add custom activities or metrics for business operations the framework cannot understand;
- configure exporters deliberately and verify failure behavior;
- preserve context propagation across supported HTTP and messaging boundaries;
- use sampling for traces with a documented objective, not as a hidden substitute for capacity planning;
- keep logs available even when a trace is not sampled, while controlling their own volume and sensitivity.
Instrumentation must match the deployed .NET and package versions. As of September 15, 2026, .NET 11 is still a release candidate. Its prerelease ASP.NET Core documentation says built-in HTTP server metrics and traces cover the required parts of the OpenTelemetry HTTP server conventions, making OpenTelemetry.Instrumentation.AspNetCore optional only for that path. It is not a drop-in equivalent: recommended attributes and SignalR or Blazor telemetry may still need the package, and tracing without it requires the Microsoft.AspNetCore activity source. Stable .NET 10 and earlier applications should follow their versioned package guidance. Test the emitted telemetry in a pre-production environment, then verify one known request after deployment.
OpenTelemetry correlation is also not automatic proof that all three signals agree. Check the actual exported resource identity, timestamps, trace context, route values, and units. Preserve both event time and the time a collector observed the record when the pipeline supports them. Account for clock skew before declaring that one event preceded another.
Start live diagnosis with dotnet-counters
dotnet-counters is intended for ad hoc health monitoring and first-level performance investigation. It reads System.Diagnostics.Metrics instruments and older EventCounters without requiring a full trace. The syntax below is documented for dotnet-counters 3.0.47001 and later; use a tool version compatible with the target runtime and process architecture.
First identify the correct process by deployment metadata or your process manager. dotnet-counters ps can help, but current versions may display command-line arguments; treat that output as sensitive if startup arguments contain credentials. Prefer process ID over a reused process name once the target is verified.
Monitor runtime metrics:
dotnet-counters monitor --process-id <PID> --counters System.Runtime
On .NET 9 and later, System.Runtime selects the runtime Meter. On .NET 8 and earlier, the same argument selects the older System.Runtime EventSource and the tool displays EventCounters instead, so the modern dotnet.* instrument names will not appear.
For an ASP.NET Core app that exposes the current hosting meter, inspect it separately:
dotnet-counters monitor --process-id <PID> --counters Microsoft.AspNetCore.Hosting
Here, Microsoft.AspNetCore.Hosting is the ASP.NET Core Meter documented for ASP.NET Core 8 and later, not a legacy EventCounter provider.
Both forms are documented in Microsoft's current dotnet-counters reference and ASP.NET Core metrics guide. The displayed instruments depend on the app runtime and enabled meters. An empty meter is evidence to verify instrumentation, process identity, and runtime version; it is not evidence that the service has no requests.
On Linux and macOS, the tool and target process must share TMPDIR for automatic diagnostic-port discovery. The tool normally needs to run as the same operating-system user as the target process or as root. Prefer the target user and the narrowest container or host access that works instead of granting broad privileges.
Observe long enough to cover the symptom but stop when the question is answered. Record the incident time, PID, runtime version, traffic window, and deployment. Counter names without that context are difficult to interpret later.
Escalate to traces or GC dumps only with evidence
Counters narrow the hypothesis. A trace or heap artifact should answer a specific remaining question, such as which methods consume CPU, which waits block ThreadPool work, or which object types remain rooted.
Collect a bounded EventPipe trace
dotnet-trace collects runtime and application events through EventPipe. Standard EventPipe is cross-platform and can attach as the same user that launched the process. Its normal scope is managed code and the runtime; it does not automatically provide network packets, kernel events, or complete native-code evidence. The separate Preview collect-linux mode in .NET 10 and later can add operating-system events on a supported Linux kernel with elevated privileges, but it is not the standard collection shown here. The EventPipe overview explains that boundary.
After verifying the PID, output path, storage capacity, and maintenance approval, a bounded default trace can be collected with:
dotnet-trace collect --process-id <PID> --duration 00:00:00:30 --output ./service-incident.nettrace
The --duration format is dd:hh:mm:ss. For dotnet-trace 9.0.661903 and later, the current dotnet-trace reference documents the default dotnet-common and dotnet-sampled-thread-time profiles when --profile, --providers, and --clrevents are all omitted. Do not copy a broad provider set from an unrelated investigation. Extra events increase collection volume and can include application payload or stack context.
Trace collection is a production action, not a harmless read. Test the chosen profile under representative load, set a short duration, watch service health and disk space, and stop if latency or resource pressure worsens. Store the result in an access-controlled location, record who collected it and why, transfer it securely, and delete it under the approved retention policy.
Use dotnet-gcdump for a managed-heap question
Choose dotnet-gcdump only after sustained managed-memory evidence points to object retention or allocation composition. The current tool applies to dotnet-gcdump 10.0 and later. A collection command is:
dotnet-gcdump collect --process-id <PID> --output ./service-memory.gcdump
Despite its description as a lower-overhead heap tool, collection triggers a generation 2 full garbage collection. Microsoft warns that this can suspend the runtime for a long time on a large heap and should not be used in a performance-sensitive environment with a large heap. The target's event buffer can grow to 256 MB, in addition to memory used by the tool. Review the current dotnet-gcdump documentation before collection.
A GC dump and a full process dump are different artifacts. A full dump can contain the entire process memory, including secrets, and Microsoft explicitly classifies it as sensitive. Treat the other artifacts as sensitive too: a GC dump exposes type names, object counts, and the object-root graph, while a trace can expose stacks, code paths, and provider payloads. Use least privilege, protected storage, limited access, documented retention, and verified deletion. Never attach any diagnostic artifact to a public ticket.
Follow evidence-based investigation playbooks
Slow requests or rising tail latency
Confirm the affected user journey, service, route template, environment, version, and time window. Compare p50 with a tail percentile such as p95 or p99, but ensure the histogram buckets and aggregation support the claimed quantile. Split by stable route and status; do not split by request ID.
Use distributed traces to locate time between the server, application, and dependencies. Use logs for the exception, timeout, retry, circuit-breaker, or business outcome tied to a representative request. Check runtime counters only after establishing that delay occurs inside the process. If application and dependency telemetry remain clean, move to DNS, connection, load-balancer, and network evidence rather than labeling the gap “network latency.”
High CPU
Verify whether CPU time rose with traffic and whether the process, container, or host is the constrained boundary. Check request throughput, latency, GC, exception, and allocation changes. If high CPU is sustained and reproducible, capture a short trace that covers the condition and inspect hot managed stacks. Microsoft's high-CPU diagnostic guide follows this counters-first, trace-second sequence.
Do not infer inefficient code from host CPU alone. Container throttling, another process, native libraries, excessive logging, GC, or a traffic shift can produce a similar symptom.
Memory growth and garbage collection pressure
Separate working set from managed heap. Compare traffic, allocation, heap size after collections, GC pause time, generation counts, pinned or large-object behavior where available, and container memory limits. Repeated growth after comparable full collections supports a retention hypothesis more strongly than one high snapshot.
Collect a GC dump only when object counts and roots will answer the next question and the service can tolerate a full collection. Escalate to a full process dump only under an approved procedure with stronger data handling, because a full dump can expose secrets held in memory.
ThreadPool starvation
Look for rising request latency alongside a growing ThreadPool thread count, queued work, and CPU below saturation. The exact pattern varies by runtime and workload. Microsoft's current ThreadPool starvation tutorial uses counters to establish the condition and a stack or trace to find blocking work.
Common code-level candidates include synchronous waiting on asynchronous operations and long blocking work on ThreadPool threads. Confirm them in stacks or trace events before changing code. Increasing minimum threads can mask the symptom and add memory or scheduling cost; it does not remove blocking.
Dependency failures
Use the application boundary as the numerator and denominator: failed dependency calls divided by all dependency calls observed by the same instrumentation. Segment by a stable dependency name, operation, status class, and caller version. Check timeouts, retries, and circuit-breaker state. Retries can reduce visible request failures while increasing dependency load and user latency.
Correlate a representative trace and structured log. Do not log full URLs, credentials, query bodies, or connection strings. If the dependency span ends before the actual failure or retry work, fix instrumentation before using it to assign ownership.
Build dashboards, alerts, and SLOs that drive action
Keep the primary service view compact:
- traffic and active requests;
- latency distribution for critical routes;
- failures and cancellations with a same-boundary denominator;
- top dependency latency and failures;
- CPU, memory, allocation, GC pause, and ThreadPool pressure;
- current release and deployment markers;
- telemetry freshness and exporter failures.
Use a metrics backend for these dashboards and SLO calculations. Logs can explain individual outcomes, but counting log lines is a fragile substitute for a defined request metric when sampling, duplicate delivery, schema changes, or missing events are possible.
Alert on sustained user impact or an imminent capacity limit with a named owner and runbook. Pair rate alerts with minimum traffic or an absolute-failure fallback so one error in an idle service does not page the team. Treat missing telemetry separately from healthy telemetry: a stopped exporter can make a bad service look quiet.
Define SLOs on user journeys or service operations, not runtime counters. Runtime pressure is diagnostic context. An SLO needs a documented event population, success condition, latency boundary where applicable, excluded traffic, data source, and behavior during missing data. Validate the calculation with controlled success and failure requests before relying on its error budget.
Operate diagnostic access safely
The .NET diagnostic port can expose detailed program state. Microsoft's diagnostic-port security guidance warns that a party with access could observe secrets in memory and modify execution. CoreCLR restricts the default port to the application user or a superuser, but that does not make every process under a shared identity mutually trusted.
Use a dedicated workload identity where practical. Do not expose a diagnostic socket through a public container port. Keep attach privileges out of the normal application role, log diagnostic access, and require an incident or approved change reference. Disabling diagnostics with DOTNET_EnableDiagnostics=0 can harden a workload, but it also prevents external dotnet-* tools; make that a deliberate operational choice and test the alternative collection path.
Before any production trace or dump:
- state the question the artifact will answer;
- confirm the exact process, replica, runtime, and architecture;
- define duration, providers, output size, stop conditions, and owner;
- watch service latency, memory, CPU, and storage during collection;
- encrypt transfer and storage and restrict access;
- document retention and verify deletion from temporary locations and tickets.
Use Fluxtail for the log portion only
Fluxtail is a paid, self-service log-management service with Starter and Pro plans. It is not a .NET APM, metrics, tracing, dashboard, SLO, or network-monitoring system. A separately configured application, sender, or collector must deliver .NET logs through a documented receiver path. Field names and filter availability depend on what that path sends and maps.
After one known event is stored, Fluxtail can provide a modern log workflow through search and filters and Live Tail. Verify service, environment, version, event ID, severity, and correlation fields in the stored row before building an investigation around them. Fluxtail cannot reconstruct a trace, duration metric, or field the source omitted.
For OpenTelemetry logs, use the OpenTelemetry logging guide to check the supported log path and expected context. Do not assume that enabling .NET OpenTelemetry metrics or tracing also sends logs, or that an arbitrary Collector configuration maps every attribute into a Fluxtail filter.
Fluxtail's built-in AI log analysis and hosted MCP are separate interfaces. Hosted MCP uses browser OAuth with PKCE and binds the connection to one account. Keep AI or agent investigations bounded to the relevant stream and time window, verify conclusions against stored rows, and review any proposed configuration mutation before applying its short-lived confirmation token.
The log-management best-practices guide covers collection, schema, security, retention, and pipeline testing. The mean time to resolution guide explains how better evidence fits into a wider incident workflow without confusing recovery with permanent resolution.
Verify the monitoring system end to end
Before calling the implementation ready:
- Send controlled successful, failed, slow, cancelled, and dependency-error requests in a safe environment.
- Confirm request count, duration units, route template, status, and active-request behavior.
- Confirm service, environment, version, and instance identity across metrics, traces, and logs.
- Verify trace propagation through each supported dependency and messaging boundary.
- Confirm structured logs exclude credentials, bodies, and sensitive identifiers.
- Restart and scale the service; check instance churn and aggregation behavior.
- Exercise the alert with normal, low, and missing-telemetry traffic.
- Verify dashboard and alert links open the correct service, environment, and time window.
- Run
dotnet-countersagainst a test process and document runtime-specific output. - Test one bounded trace outside peak traffic and validate secure artifact handling.
- Keep GC-dump and full-dump procedures disabled from casual use; rehearse them only under an approved production-like plan.
- Verify a known .NET log in the central backend and confirm its actual searchable fields.
The result should be a short path from symptom to evidence: detect user impact with metrics, localize it with traces, explain it with logs, inspect the runtime with counters, and collect sensitive diagnostics only when the preceding evidence justifies them.