Finding .NET Memory Leaks and CPU Hotspots with Visual Studio Diagnostic Tools
Learn how to use Visual Studio's Diagnostic Tools window and Performance Profiler to spot memory leaks and CPU bottlenecks in .NET apps, with a concrete example and practical verification steps.
06 Jan 2026, 13:29 UTC

When a .NET service starts consuming more memory over time or shows unexplained CPU spikes, the first step is to confirm whether the issue lives in managed code. Visual Studio 2022 (version 17.8 or later) bundles two complementary views that let you observe the problem while debugging and then dig deeper with offline analysis: the Diagnostic Tools window for real‑time telemetry and the Performance Profiler for heap snapshots and CPU profiling. This post walks through a concrete leak scenario, shows how to use each view, explains the trade‑offs between sampling and instrumentation, and gives a practical way to verify that a fix works.
1. Spot the problem live with the Diagnostic Tools window
Open your solution, set a breakpoint where the suspicious work begins, and start debugging (F5). While the debugger is paused or running, the Diagnostic Tools window appears at the bottom (if not, choose Debug → Windows → Show Diagnostic Tools). It displays two live graphs:
- CPU Usage – a sampling‑based percentage of processor time.
- Memory Usage – total managed heap size and breakdown by generation.
If the memory graph climbs steadily while CPU stays low, you likely have a leak rather than a tight loop. Keep the window open; you can hover over any point to see the exact heap size at that moment.
2. Compare heap states with Performance Profiler snapshots
To identify which objects are accumulating, stop the debugger (or keep it running) and launch the Performance Profiler:
- Choose Debug → Performance Profiler….
- In the dialog, tick Memory Usage and click Start.
- Perform the scenario that you suspect causes the leak (e.g., upload a file, process a batch).
- Click Take Snapshot before and after the workload.
The tool then shows a diff view: objects with increased instance count appear at the top. For a typical leak caused by a static collection, you’ll see a surge in System.String or your custom type.
Worked example: a leaking static list
Consider this minimal console app:
using System;
using System.Collections.Generic;
class Program
{
private static readonly List _leak = new List();
static void Main()
{
while (true)
{
_leak.Add(Guid.NewGuid().ToString()); // leak
System.Threading.Thread.Sleep(100);
}
}
}
Run the app under the Memory Usage tool, take a snapshot after 10 seconds, then another after 30 seconds. The diff will show System.String instances growing by roughly 200 per snapshot, confirming the leak.
3. CPU hotspots: sampling vs. instrumentation
If CPU usage is high, switch the Performance Profiler to CPU Usage. You have two collection methods:
- Sampling (default) – lightweight, periodic stack walks; good for finding hot paths with ≈1 ms resolution.
- Instrumentation – inserts entry/exit probes; gives exact call counts and elapsed time but can add 10‑100× overhead, potentially skewing timing‑dependent code.
For most .NET apps, start with sampling. If you need precise call counts (e.g., to verify a refactor reduced a specific method’s invocations), enable instrumentation, run a short scenario, and then compare the counts before/after the change.
4. Limitations and verification steps
Remember these caveats:
- Debug vs. Release – Debug mode disables many optimizations, inflating object sizes and hiding inlined calls. For performance data, switch to Release configuration (Ctrl+F5 or Debug → Start Without Debugging) before profiling.
- Snapshot freeze – Taking a memory snapshot pauses the app while the heap is walked; large heaps can cause a noticeable pause (seconds). Limit snapshots to intervals you can tolerate.
- Instrumentation overhead – Only enable it when you need call‑count accuracy; otherwise stick with sampling.
To verify that a fix works, repeat the exact same steps:
- Build the solution in Release.
- Run the Memory Usage tool, take a before‑snapshot, execute the scenario, take an after‑snapshot.
- Confirm that the instance count delta for the suspect type is now near zero (or matches expected growth from legitimate caching).
- Optionally, check the CPU Usage graph to ensure no new hot path appeared.
If the delta remains high, revisit the code: look for static fields, event handlers not unsubscribed, or caches without eviction policies.
Closing checklist
- Use Diagnostic Tools window for immediate, low‑overhead feedback while debugging.
- Launch Performance Profiler → Memory Usage to capture heap snapshots and compare them.
- Start with CPU sampling; move to instrumentation only when you need exact call counts.
- Always validate in Release mode and be aware of snapshot‑induced pauses.
- After a fix, repeat the snapshot comparison to confirm the leak is gone.
By combining real‑time telemetry with snapshot‑based diffs, you can turn a vague “memory keeps growing” complaint into a precise line of code—and do the same for elusive CPU bottlenecks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.