From the GoReplay team

GoReplay reproduces production bugs. Proof catches them before production.

See Proof
Published on 10/30/2026

.NET Application Performance Monitoring Explained

.NET Application Performance Monitoring Explained

You shipped the build, the dashboards stayed green, and then checkout started timing out for a slice of customers. CPU looks calm. Average response time looks calm. The people waiting do not.

That gap is what .NET application performance monitoring is supposed to close. Host charts tell you the service is up. They don’t tell you why one route stalls for one region after one release. The useful version names the signals, shows where the runtime exposes them, and proves a candidate build still behaves like production before you ship it.

What .NET Performance Monitoring Covers and Why It Matters

A car dashboard shows speed and fuel. A mechanic still has to know why the engine knocks under load. .NET application performance monitoring is that mechanic’s view: continuous measurement of how an application’s requests behave in production, covering how long they take, which calls they wait on, what the runtime is doing (CPU, memory, garbage collector, thread pool), and where errors begin.

Detection and diagnosis are different jobs. Monitoring shows that checkout is slow. Diagnosis shows which dependency, method, or release causes it. A 200 ms average can sit beside a tail where about 2 percent of requests take 4 seconds. Those are the users who leave, and an average chart will never name them.

In 2010, Google published its Dapper paper, documenting a production distributed-tracing system that had already been deployed and used for more than two years. Dapper followed one transaction across services and attached timing to individual operations, instead of stopping at isolated server metrics. Zipkin, Jaeger, and OpenTelemetry came later as separate projects. The working rule stayed the same: instrument continuously, propagate request context, and find latency in the trace rather than in uptime alone.

The order from here is practical. Metrics and alert points, then network quality, instrumentation choices, traces and logs, bottleneck checks, replay against a real workload, and a baseline you can set this week.

Key .NET Metrics and the Thresholds to Alert On

These nine signals are starting points. Tune every threshold against your own baseline. A limit that is noisy on one API is late on another.

MetricWhat It RevealsStarting Alert PointWhere to Read It
p95 and p99 request latencyTail slowness that averages hideBreaches your SLO for 5 minutesAPM histograms or ASP.NET Core request metrics
Error rateFailed requests and unhandled exceptionsAbove 1% of requests for 5 minutesRequest logs and exception counters
CPU utilizationProcess or host saturationSustained above 70-80% of available coresdotnet-counters or host metrics
Working set and private bytesMemory footprint and possible leaksRising across load cycles without plateaudotnet-counters and heap snapshots
GC pause time and gen 2 collectionsTime threads pause for garbage collectionGC above about 10% of CPU timeRuntime GC counters
Thread pool queue lengthWork waiting for a free worker threadQueue growing for seconds under steady loadThread pool counters
Database call duration and countSlow or chatty data accessQuery count per request above normal baselineAPM database spans
Outbound HTTP call durationSlow or failing downstream servicesp95 above the caller’s timeout budgetHttpClient telemetry
Connection pool usagePool exhaustion before CPU limitsActive connections near maximum pool sizeSQL and HttpClient counters

Latency belongs on p95 and p99 per endpoint. Error rate counts 5xx responses and unhandled exceptions per endpoint and per app version, so a bad deploy doesn’t hide inside a healthy fleet average. CPU above roughly 70 to 80 percent of available cores, held there, leaves no headroom for a spike. Working set and private bytes should plateau after warm-up. A line that climbs across load cycles is a leak until a snapshot says otherwise.

GC pause time near 10 percent of CPU time, plus a rising gen 2 count, means threads are waiting on the collector. A thread pool queue that grows for seconds under steady load means work is queued, not running. Database and outbound HTTP duration and count per request catch chatty data access and slow partners. Connection pools often exhaust before CPU moves, so watch active connections against the configured maximum.

Histograms beat averages

OpenTelemetry’s metrics model treats a histogram as the instrument for request latency, including counts of requests under a defined threshold. Averages hide a small, operationally painful set of slow calls. Buckets support SLO math and percentile alerts. Exemplars and shared resource attributes connect a spike to a service, deployment, host, or trace.

Tag each request with route, status, version, region, and a correlation id. Export latency histograms, request counts, error counts, and network read and write metrics. Compare the distributions before and after a deploy so you can tell application time from a dependency or the network.

What to page on first

Page on the user-facing SLO first: latency, errors, availability. Page on CPU, GC, pools, and queues second. Runtime charts explain a breach. They shouldn’t wake someone when users are fine.

Practical rule: If p99 is inside the SLO and errors are flat, a high CPU reading is a capacity note, not a page.

Network QoS Signals Behind Slow .NET Requests

Bandwidth is a poor predictor of whether a .NET request feels fast. A link can move more bytes and still deliver a worse request, because the app waits on delay, variation, and loss, not on the size of the pipe.

A controlled comparison of traditional and virtualized networks makes the split concrete. In that comparison, published by the Journal of Information Security and Information Systems, latency was 25 ms versus 37 ms, jitter 5 ms versus 8 ms, packet loss 0.5% versus 0.9%, throughput 88 versus 92 Mbps, and availability 99.8% versus 99.5%. The virtualized side had the higher throughput and the worse latency, jitter, loss, and availability. Extra megabits did not pay for the rest.

Put the two families of signals side by side. Application histograms say the request got slower. Host CPU, GC, and thread-pool counters say whether the process is the reason. Network quality says whether the path between hosts changed.

  1. Record the path. Capture latency, jitter, packet loss, and retransmissions for each host pair that serves the app.
  2. Share the window. Attach those values to the same time range as the request histograms and the trace spans.
  3. Check the network before the profiler. When p95 rises and CPU and GC stay flat, inspect path quality before you sample the process.
  4. Separate code from path in replay. Compare read and write network metrics on the baseline build and the candidate, so a path regression isn’t filed as an application bug.

Alert on percentile latency, loss, and jitter together. A bandwidth threshold alone will stay quiet while users wait.

Choosing Between Profilers, APM Agents and OpenTelemetry

A slow request in production does not always need a profiler attached. Choose the collection method based on whether you need a one-time code-level diagnosis or continuous visibility, and on who will maintain the instrumentation and backend.

A diagram comparing on-demand profilers, APM agents, and OpenTelemetry for monitoring application performance signals and code ownership.

ApproachSetup EffortData It Gives YouBest FitMain Risk
On-demand profiler (dotnet-trace, dotnet-counters)Low, one commandHot methods, allocations and GC events for one sessionInvestigating a known slow processNo history, and overhead while attached
Commercial APM agentLow to medium, auto-instrumentedTraces, metrics, errors and dashboardsTeams wanting a managed backend quicklyLicence cost and vendor lock-in
OpenTelemetry SDK with CollectorMedium, you choose exportersTraces, metrics and logs in one standard formatMulti-service estates and backend flexibilityYou own sampling, dashboards and backend choice
Hybrid: OpenTelemetry plus profilerMedium to highContinuous signals plus deep divesTeams with steady production traffic and targeted investigationsTwo toolchains to maintain

On-demand profilers such as dotnet-trace and dotnet-counters attach to a live process and capture hot methods, allocations, and GC events. They are useful when another signal has narrowed the problem to a process, but they provide no continuous history and add overhead while attached. Keep the capture focused, then detach when it answers the question.

Commercial APM agents auto-instrument ASP.NET Core, Entity Framework, and HttpClient, providing traces, metrics, errors, dashboards, and alerts after installation. The trade-off is licence cost and dependence on the vendor’s instrumentation and backend. Measure overhead under production-like concurrency in staging before rollout, and account for the work of redoing instrumentation if you switch vendors.

The OpenTelemetry .NET SDK and Collector provide a standard route for exporting traces, metrics, and logs to a backend you choose. That flexibility shifts operational work to your team: you configure sampling, dashboards, and retention, and maintain the Collector and backend.

A practical hybrid uses OpenTelemetry for continuous production signals and a profiler when a trace points to work inside the process. To prove those signals reflect real user workloads, compare production traces with repeatable pre-release tests that replay representative traffic. If the same request paths and bottlenecks appear, the test is a useful release baseline; if they do not, investigate differences in traffic or environment before trusting the result.

For a small team with a few hosts, start with counters and a time-boxed APM trial. For a multi-service estate that may change backends, standardize on OpenTelemetry. In either case, compare overhead at production-like concurrency and keep the chosen tool focused on the question it can answer.

Tracing and Logging Strategies That Connect Requests

A checkout request can call cart, payment, and inventory services in sequence. If its trace shows payment taking 2.3 seconds while the other spans finish in under 50 milliseconds, the trace tree points to the slow hop.

A trace groups timed spans into a parent-child tree. Logs connect to that tree when each entry carries the matching trace and span identifiers.

Keep the tree and the log line together

  • Propagate context. Send the W3C traceparent header on every HttpClient call, including calls between internal services. A missing hop splits the trace.
  • Add useful span attributes. Record route, status code, app version, region, and deployment ID. These bounded fields let you filter slow requests by release without creating a separate trace for every request value.
  • Correlate logs. Include trace ID and span ID in each log entry through Activity or a logger enricher. Structured templates make fields easier to search than concatenated strings.
  • Set severity by meaning. Log business events at Information and failures at Warning or Error. Exclude personal data and full payloads.
  • Choose sampling deliberately. Fixed-rate head-based sampling limits collection cost, but may miss rare failures. Tail-based sampling in a Collector can retain errors and slow traces, at the cost of more Collector resources. Keep error and slow-request traces, and sample healthy traffic.

If responders cannot open a failed request’s trace within five minutes, check whether trace IDs survive every service and logging boundary before adding dashboards.

Cardinality affects ingestion and storage costs. Keep span and metric attributes bounded, such as route templates, rather than using raw order IDs or user names. Estimate monthly cost using spans per request, request volume, and retention before enabling full capture.

A 2025 Checkmk survey of 192 IT professionals in 27 countries found that 61% called rising complexity their biggest challenge. More traces and alerts can increase cost and noise without speeding diagnosis. After changing sampling, compare SLO compliance, MTTR, and escaped defects, using the same period to assess full-fidelity traces against targeted sampling. Logging and monitoring practices are more useful when log entries and spans share identifiers and the captured traffic reflects workloads worth replaying.

Common Bottlenecks and Their First Checks

Match the symptom before you change code. Most production slowdowns in .NET land in one of seven patterns.

A diagram illustrating common performance bottlenecks in applications and recommended first checks for each scenario.

SymptomLikely .NET CauseFirst Check
Latency rises only under loadThread pool starvation from blocking callsThread pool queue length and blocked thread count
Memory climbs and never fallsEvent handler leaks or unbounded cachesCompare two heap snapshots under similar load
Pauses every few secondsFrequent gen 2 collections from large allocationsGC pause time and allocation rate
One endpoint slowN+1 queries or a missing indexQuery count and duration per trace
Timeouts to a partner APIDownstream latency or HttpClient connection exhaustionOutbound p95 and connection pool counters
CPU at 100% with flat throughputTight loops, regex backtracking or large payload serializationCPU profile captured at peak
Errors right after a deployConfiguration or dependency version changeError rate compared by version tag

Latency that appears only under load is usually thread pool starvation from blocking calls or sync-over-async. Memory that never falls back points at event-handler leaks, unbounded caches, or retained collections. Compare two heap snapshots taken under similar load, not one snapshot after a quiet period. Pauses every few seconds usually mean large allocations driving gen 2 collections. One slow endpoint is often N+1 queries or a missing index, visible as query count on a single trace. Partner timeouts are downstream latency or HttpClient pool exhaustion. CPU pinned at 100 percent with flat throughput is a hot loop, catastrophic regex backtracking, or huge payload serialization. Errors that start at deploy time are configuration or a dependency version, which is why the version tag has to be on the error counter.

Work the symptom in order

  1. Confirm the user-facing breach against the SLO, not against a single slow sample.
  2. Scope it by endpoint, version, and region so you aren’t profiling the healthy half of the fleet.
  3. Read runtime counters for CPU, GC, thread pool, and pools before you open a profiler.
  4. Open one slow trace and see whether time sits in your code, the database, or an outbound call.
  5. Profile at peak only if the trace points inside the process. A CPU sample of an idle box wastes the afternoon.

Change one variable, then measure again. Two fixes in one deploy leave you guessing which one worked.

Using Traffic Replay and Load Tests in a QA Process

Scripted load tests cover the requests someone imagined. Production sends the mix users generate, including odd headers, retries, and bursts no script author wrote down. Dashboards of that traffic don’t prove the next build will survive it. A replay does, if the replay is faithful.

A professional analyzing load test results and API traffic logs on dual computer monitors in an office.

The loop is short. Capture production HTTP traffic, or a representative slice of one critical flow. Replay it into staging at the same rate and concurrency. Compare p95, p99, error rate, GC time, and thread pool queue length with the baseline build. GoReplay is one open-source way to capture and replay that HTTP traffic from a file, so both builds see the same workload. Replay is HTTP-level. Confirm that headers, host names, and session state still reach the .NET service intact, or the comparison is fiction.

Recent research on application-level replay found that naively replaying recorded production traces achieved a success rate below 80%, while templating dynamic values raised replay success to 98.3% across 291 attempts. Stale timestamps, reused idempotency keys, and other state-dependent fields were the failures. A pretty latency chart from a replay that mostly 400s is not evidence.

A readiness checklist before you trust a pass

  • Mask personal data before the capture leaves production.
  • Substitute tokens, timestamps, and idempotency keys so the run isn’t rejected for stale state.
  • Disable side effects such as payments and email, or point them at fakes.
  • Tag replayed requests with the app version under test.
  • Set pass and fail thresholds before the run, using the baseline distributions.
  • Check fidelity. The replayed latency shape should resemble the original, not just return 200s.

How traffic replay improves load testing accuracy is the difference between a script you invented and a workload you recorded. For capacity, step the request rate up until p95 breaches the target. That knee is real capacity for this mix, not a round number from a spreadsheet.

A walkthrough of reading those curves against a live replay is easier to follow than a screenshot alone.

A pass means the candidate matches the baseline within the thresholds you set, not that the service is fast in absolute terms.

Confirm Your Baseline Before the Next Release

You can start this week. The point is a number the team can read in five minutes, and a release that has to match it.

  1. Pick three user-facing SLOs on each critical endpoint: p95 latency, error rate, and availability.
  2. Turn on the runtime counters from the metrics table on every instance, not only on the box you happen to SSH into.
  3. Choose one instrumentation route and export traces with the trace id written into logs.
  4. Capture one day of one critical flow and replay it into staging with masked data and side effects disabled.
  5. Record the baseline p95, p99, error rate, and GC time for the current build.
  6. Gate the next release on the candidate staying inside those thresholds. File-based HTTP replay is enough for the capture and the gate. Start with a single critical flow.

I would replay checkout first. It crosses cart, payment, and inventory, so it exposes the most latency and error signals in one pass. Add other flows only after the checkout baseline holds for two consecutive releases.


GoReplay captures live HTTP traffic and replays it into a test environment so you can compare latency, errors, and runtime signals on a real request mix before a .NET release. That is the missing check when dashboards and averages still look fine. See how to start from one critical flow at GoReplay.

Ready to Get Started?

Join these successful companies in using GoReplay to improve your testing and deployment processes.

Talk to the GoReplay team

Describe what you want to capture or replay, your deployment, and any PRO requirements. Or email [email protected].

Please leave out credentials and production request data.