.NET Academy

Performance and diagnostics

Measure before optimizing — benchmarks, profilers, dotnet-counters, and the most common performance fixes.

13 min read

You will learn

  • Measure performance correctly with BenchmarkDotNet
  • Diagnose live apps with dotnet-counters, dotnet-trace and dotnet-dump
  • Recognize the most common backend performance problems
  • Prioritize optimizations by impact

Measure first

"Premature optimization is the root of all evil" — not because performance doesn't matter, but because intuition about bottlenecks is usually wrong. The workflow is always:

  1. Define the goal — "p95 latency of GET /products under 200 ms at 500 requests/s".
  2. Measure — find where the time actually goes.
  3. Fix the biggest problem.
  4. Measure again to confirm.

Where backend time usually goes

In rough order of how often they're the culprit:

  1. Database: N+1 queries, missing indexes, fetching whole entities when you need three columns, no pagination.
  2. External calls: slow third-party APIs called sequentially instead of concurrently, no timeouts, no caching.
  3. Blocking: sync-over-async (.Result) starving the thread pool.
  4. Allocations: huge object graphs, large strings, boxing in hot loops → GC pressure.
  5. Actual CPU work: rarely the problem in typical business apps.

So check the database and I/O first — you already know the fixes: Include/projections, indexes, caching, Task.WhenAll, async all the way.

Micro-benchmarks with BenchmarkDotNet

To compare two implementations of the same code precisely, use BenchmarkDotNet. It handles JIT warm-up, runs many iterations, and reports statistics and allocations:

[MemoryDiagnoser]
public class ParsingBenchmarks
{
    private const string Line = "12,7,30,1,99,42";
 
    [Benchmark(Baseline = true)]
    public int Split() => Line.Split(',').Sum(int.Parse);
 
    [Benchmark]
    public int Span()
    {
        int total = 0;
        foreach (var range in Line.AsSpan().Split(','))
            total += int.Parse(Line.AsSpan()[range]);
        return total;
    }
}
 
// Program.cs — run with: dotnet run -c Release
BenchmarkRunner.Run<ParsingBenchmarks>();
| Method | Mean      | Ratio | Allocated |
|------- |----------:|------:|----------:|
| Split  | 112.40 ns |  1.00 |     328 B |
| Span   |  38.15 ns |  0.34 |         - |

Always benchmark in Release mode.

A rough comparison in your browser

A Stopwatch loop can't replace BenchmarkDotNet, but it's enough to see large differences:

Program.cs
Loading editor…

C# runtime not loaded

Diagnosing a running app

.NET ships command-line diagnostic tools (dotnet tool install -g dotnet-counters etc.):

ToolWhat it shows
dotnet-counters monitor -p <pid>Live metrics: CPU, GC heap size and collections, thread-pool queue, exceptions/s, requests/s
dotnet-trace collect -p <pid>A CPU/event trace to open in Visual Studio, PerfView or speedscope
dotnet-dump collect -p <pid>A memory dump for analyzing leaks or hangs
dotnet-gcdumpA lightweight heap snapshot: which objects use memory

Visual Studio and JetBrains profilers (dotTrace, dotMemory) show the same data with a UI. In production, OpenTelemetry traces and metrics (Application Insights, Grafana) tell you which endpoints and dependencies are slow.

Load testing

Before launch, simulate real traffic with k6, NBomber (C#), JMeter or Azure Load Testing. Watch latency percentiles (p50, p95, p99), error rates and resource usage as load increases — that's how you find the limits you explored in the scaling simulator.

Built-in performance features worth knowing

  • Response compression and output caching middleware.
  • HybridCache / IDistributedCache with Redis.
  • Native AOT for fast startup and small containers.
  • System.Text.Json source generators, [LoggerMessage], Span<T>, ArrayPool<T> for hot paths.
  • Every .NET release brings large free performance improvements — staying on a current version is an easy win.

Percentiles, not averages

An average of 50 ms can hide 5% of users waiting 3 seconds. Track p95/p99 latency — the slowest requests are often the ones your users remember.

Key takeaways

  • Measure before optimizing; set concrete goals and verify each fix.
  • Most backend time is spent in the database and external I/O — fix those first.
  • Use BenchmarkDotNet (in Release) for micro-benchmarks, dotnet-counters/trace/dump and profilers for running apps.
  • Load test before launch, and watch latency percentiles rather than averages.

Quiz

4 questions · 3 correct to pass
  1. 1.Why is a quick Stopwatch loop a poor way to compare two implementations precisely?

  2. 2.Your API is slow under load. What should you do first?

  3. 3.Which tool shows live runtime metrics (GC, thread pool, exceptions, request rate) of a running .NET process?

  4. 4.Which change typically has the biggest impact on a slow data-driven endpoint?

To complete this lesson

  • ○ Pass the quiz (3/4 correct)