📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

A problem only happens in production and you cannot attach a debugger. How do you capture and analyse a memory dump of a .NET process?

Production diagnosis with dumps: when to take one, capturing with dotnet-dump, createdump settings, App Service and Kubernetes options, analysing in dotnet-dump or Visual Studio, and handling dumps as sensitive data.

Senior Debugging Expert dotnet-dump incident-response memory-dump production-debugging sos

🎯 Key points

  • A dump is a snapshot of process memory, threads and stacks that you analyse offline without pausing production for long
  • Use it for hangs, deadlocks, memory leaks and crashes; use counters and traces for slowness
  • Capture with dotnet-dump collect, crash dumps via DOTNET_DbgEnableMiniDump settings, App Service diagnostics, or dotnet-monitor in Kubernetes
  • Choose the dump type: full or heap dumps for memory issues, smaller ones for stacks only; collection briefly suspends the process
  • Analyse in dotnet-dump analyze (clrthreads, clrstack -all, dumpheap -stat, gcroot, pe, dumpasync) or open it in Visual Studio with Threads, Call Stack and Parallel Stacks
  • Matching PDBs and the same runtime version make stacks readable
  • Dumps contain secrets and personal data: restrict access, encrypt, and delete after the investigation

⏱ 30-second answer

When I can't attach a debugger, I take a dump: a snapshot of the process's memory, threads and stacks that I analyse offline. For a hang or a leak, dotnet-dump collect -p <pid> does it in seconds. For crashes, I enable automatic crash dumps with the DOTNET_DbgEnableMiniDump environment variables. On App Service I use Diagnose and solve problems, and in Kubernetes dotnet-monitor. Then I analyse with dotnet-dump analyze: clrstack -all and syncblk for hangs, dumpheap -stat and gcroot for leaks, pe for exceptions. Or I open the dump in Visual Studio and use the Threads and Parallel Stacks windows. Dumps contain customer data, so they're handled as sensitive.

🗣 Full answer (senior)

How I would answer this in an interview

"When I can't attach a debugger, I take a dump, a snapshot of the process's memory, threads and stacks that I analyse offline. I use dumps for hangs, leaks and crashes, and counters or traces for plain slowness. I capture with dotnet-dump collect, automatic crash dumps through the DOTNET_DbgEnableMiniDump settings, App Service diagnostics or dotnet-monitor in Kubernetes, picking a heap or full dump for memory issues and a smaller one when stacks are enough, and I keep matching PDBs and the runtime version so the stacks are readable. For a hang I take two dumps a few seconds apart and compare clrstack -all, in dotnet-dump analyze or Visual Studio's Parallel Stacks, which often points to a deadlock; for a leak I compare two dumps at different sizes and follow a few object addresses back to their roots.

For example, [our reporting API on App Service] grew to over [3 GB] every day and we were relying on a nightly recycle. I wanted to remove the root cause rather than keep the workaround. I took two heap dumps [six hours] apart, compared them with dumpheap -stat, and used gcroot to trace the growing objects to an unbounded static cache keyed by raw query strings. I replaced it with a size-limited MemoryCache with normalised keys, and added a memory alert and a soak test. Memory levelled off at about [600 MB] and we removed the nightly recycle.

Two cautions. Collection briefly suspends the process, so I take it from one instance, out of the load balancer if I can. And dumps contain tokens and customer data, so I treat them as sensitive: restricted access, encrypted storage, deleted when the investigation ends. The mistake I see is restarting before capturing anything, which destroys the evidence, so I put the capture steps in the runbook before the incident: capture first, restart second."


When a dump is the right tool

Symptom First tool Dump?
Slow but working APM, dotnet-counters, dotnet-trace Rarely
Hang or deadlock Counters to confirm, then dump Yes, one or two a few seconds apart
Memory keeps growing dotnet-counters (GC heap), dotnet-gcdump Yes for full detail, ideally two at different sizes
Crash (process exits) Crash dump on failure Yes, automatic

Capturing

  • Live process: dotnet-dump collect -p <pid> --type Full (or Heap for memory investigations, Mini for stacks only). Collection suspends the process briefly; a full dump of a large process can take seconds and be several GB.
  • On crash: set DOTNET_DbgEnableMiniDump=1, DOTNET_DbgMiniDumpType=4 (full; 2 is heap, 1 is mini) and DOTNET_DbgMiniDumpName=/dumps/%e.%p.dmp so the runtime writes a dump when the process crashes.
  • Azure App Service: Diagnose and solve problems can collect and analyse a memory dump on demand or on a rule; on Linux, dotnet-dump is available through the SSH console.
  • Kubernetes: run dotnet-monitor as a sidecar with an authenticated endpoint, or attach an ephemeral debug container that shares the process namespace and run dotnet-dump there.

Analysing

In dotnet-dump analyze <file>:

  • clrthreads and clrstack -all: what every managed thread is doing (hangs).
  • syncblk: Monitor locks and owners (deadlocks).
  • dumpasync: pending async state machines (async hangs where no thread is blocked).
  • dumpheap -stat, then dumpheap -type <Type> and gcroot <address>: what fills the heap and what keeps it alive (leaks).
  • pe: print the current or a given exception (crashes).

Or open the dump in Visual Studio (File → Open → the .dmp, then Debug with Managed Only): the Threads, Call Stack and Parallel Stacks windows work as if the process were paused, and memory analysis views show the heap.

Making dumps useful

Keep PDBs with the deployment or in a symbol server, and note the exact runtime version. Analyse on a machine with the matching runtime, or let dotnet-dump download the needed DAC files. A dump from a Linux container is best analysed with dotnet-dump (any OS) or Visual Studio.

Production example

An ASP.NET Core API on App Service Linux grew from 400 MB to 3 GB over a day and was recycled nightly. Two heap dumps six hours apart showed millions of CacheEntry objects; gcroot led to a static MemoryCache used with unbounded keys built from query strings. Adding a size limit, sliding expiry and normalised keys stopped the growth.

🔍 What the interviewer is checking

The interviewer is assessing production maturity: can you diagnose without a debugger, safely?

Green flags

  • Picks dumps for hangs, leaks and crashes and other tools for slowness.
  • Knows capture options per platform and the dotnet-dump commands.
  • Explains leak analysis via gcroot and hang analysis via paired dumps.
  • Treats dumps as sensitive data.

Red flags

  • "Restart and add logging."
  • Wants to attach Visual Studio to production.
  • No awareness of the data in a dump.

📚 Learn in depth

1. What is inside a dump

 process memory snapshot (.dmp / core file)
 +-- threads: registers + native and managed stacks
 +-- managed heap: gen 0/1/2, Large Object Heap, Pinned Object Heap
 |     every object, its fields and its references
 +-- loaded modules: DLL images and versions
 +-- runtime data structures: method tables, sync blocks,
                              thread pool state, GC handles, finaliser queue

 analysis = dump + DAC (the runtime's data access component for THAT
            runtime build) + PDBs
          --> readable stacks, objects, roots and exceptions

The DAC (mscordaccore) knows how to read the runtime's internal structures for one specific runtime build. That is why the runtime version matters, and why dotnet-dump and Visual Studio download the matching DAC automatically when they can.

2. Choosing the right diagnostic

Symptom Start with Take a dump? Dump type
Slow but working APM, dotnet-counters, dotnet-trace Rarely n/a
Hang or deadlock dotnet-counters to confirm no progress Yes, two a few seconds apart Mini or Full (stacks are enough; Full if you also need object values)
Memory keeps growing dotnet-counters GC heap size, dotnet-gcdump Yes, two at different sizes Heap or Full
Crash (process exits) Automatic crash dump Yes Full or Heap
OutOfMemoryException Crash dump plus analyzeoom Yes Full
High CPU dotnet-trace Occasionally, several in a row Mini

dotnet-gcdump is a lighter option for leaks: it records only types, counts, sizes and references, not object contents, so it is much smaller and holds far less sensitive data. Use a full dump when you need field values.

3. Dump types

Type (--type / DOTNET_DbgMiniDumpType) Contains Size Use
Mini (1) Threads, stacks, modules, minimal runtime data Small Hangs, crashes where stacks are enough
Heap (2) Mini plus the managed heap Large Leaks, object state
Triage (3) Mini with personal data removed as far as possible Smallest Sharing more widely
Full (4) All process memory Largest (roughly the working set) When in doubt, and for native issues

4. Capturing, platform by platform

Platform Live process On crash
Any machine with the SDK tools dotnet-dump collect -p <pid> --type Heap DOTNET_DbgEnableMiniDump=1 and friends
Windows server dotnet-dump, procdump -ma <pid>, or Task Manager > Create dump file Windows Error Reporting LocalDumps or the runtime variables
Azure App Service Diagnose and solve problems > Collect Memory Dump; on Linux also dotnet-dump over SSH Runtime variables in app settings, writing to /home/dumps
Docker docker exec into the container and run dotnet-dump (installed or copied in) Runtime variables, writing to a mounted volume
Kubernetes dotnet-monitor sidecar sharing the diagnostics socket in /tmp, or an ephemeral debug container sharing the process namespace Runtime variables, writing to a persistent volume

The crash dump variables:

Variable Meaning
DOTNET_DbgEnableMiniDump=1 Write a dump when the process crashes
DOTNET_DbgMiniDumpType=2 or 4 Heap or Full
DOTNET_DbgMiniDumpName=/dumps/%e.%p.dmp File name; %e is the executable, %p the PID
DOTNET_CreateDumpDiagnostics=1 Log what the dump writer is doing, for troubleshooting

dotnet-dump and dotnet-monitor talk to the runtime through its diagnostics IPC channel. If the app sets DOTNET_EnableDiagnostics=0, they can't connect.

5. What collection costs

 dotnet-dump collect
   --> runtime suspends the process
   --> dump writer copies memory to disk     (seconds for a large heap)
   --> process resumes
  • A full dump of a 4 GB process writes roughly 4 GB and can pause the process long enough to fail health checks.
  • On a multi-instance service, take the dump from one instance, ideally after removing it from the load balancer.
  • Check there is enough disk space where the dump is written.

6. The analysis toolkit

Inside dotnet-dump analyze <file>:

Command Question it answers
clrthreads Which managed threads exist, and which hold locks or have exceptions
clrstack -all What every managed thread is doing
syncblk Which thread owns each contended Monitor lock
dumpasync Which async operations are pending and what they await
threadpool Thread pool state, queue length, worker counts
dumpheap -stat Which types use the most memory
dumpheap -type <Type> -short Addresses of objects of one type
gcroot <address> What keeps an object alive
dumpobj <address> Field values of one object
eeheap -gc Size of each GC heap and generation
finalizequeue Objects waiting for finalisation
analyzeoom Why the last OutOfMemoryException happened
pe The current or a given exception, with stack
setsymbolserver -ms Download symbols from the Microsoft symbol server

Or open the dump in Visual Studio (File > Open, then Debug with Managed Only). The Threads, Call Stack, Parallel Stacks and Tasks windows work as if the process were paused, and the managed memory view sorts types by size and shows paths to root.

7. Leak analysis, step by step

 dump A (800 MB)                      dump B (2.5 GB, six hours later)
 dumpheap -stat                       dumpheap -stat
   Count    TotalSize  Type             Count      TotalSize  Type
   120,000  490 MB     Byte[]           610,000    2.4 GB     Byte[]
   120,000  9 MB       Entry<...>       610,000    45 MB      Entry<...>
                              |
                              v  the type that grew most
 dumpheap -type System.Byte[] -short   --> pick a few addresses
 gcroot 00007f3a2c01d2a8
   static field LeakyCache.Entries
     -> ConcurrentDictionary -> Node -> Byte[]
                              |
                              v
 the holder: an unbounded static dictionary keyed by raw query strings

Common holders:

Root Typical cause
Static field Unbounded cache or registry
Event handler Subscribed and never unsubscribed, so the publisher keeps the subscriber alive
Timer A timer callback capturing this
GC handle (pinned) Buffers pinned for interop and never freed
Finaliser queue Objects with finalisers created faster than the finaliser thread can run them
DI singleton A singleton holding scoped services or per-request data

8. Hang analysis, step by step

  1. Take two dumps a few seconds apart.
  2. Run clrstack -all in both. Threads with identical stacks in both are stuck; threads whose stacks changed are working.
  3. For stuck threads in Monitor.Enter, run syncblk to find the owner. For threads in Lock.Enter (.NET 9+ System.Threading.Lock), read the stacks, because syncblk won't list it.
  4. If no thread is blocked but requests never finish, run dumpasync to find the pending async chain.
  5. Run threadpool to check for starvation (a long queue, all workers busy).

9. Making dumps readable

  • Keep PDBs with every deployment, or in a symbol server, so stacks show file and line.
  • Record the exact runtime version deployed (dotnet --list-runtimes in the image, or RuntimeInformation.FrameworkDescription in a log at start-up).
  • dotnet-dump works on any OS for dumps from any OS. A Linux core dump can be analysed on Windows and the reverse.
  • dotnet-symbol -o ./symbols <dump> downloads matching symbols for the modules in the dump.

10. Security and compliance

A full dump contains everything in memory: access tokens, connection strings, decrypted payloads and customer records. Under the Privacy Act 1988 and the Australian Privacy Principles that is personal information you are responsible for.

  • Decide in advance who may collect dumps, and log every collection.
  • Store dumps encrypted in a controlled location, for example a private storage account in Australia East with short retention.
  • Never attach dumps to tickets, chat or email.
  • Prefer dotnet-gcdump or Triage dumps when they are enough.
  • Expose dotnet-monitor only with authentication and never publicly.
  • Delete dumps when the investigation ends.

11. .NET 11 awareness

The dump format and the diagnostics tools are stable and versioned with the runtime. Update dotnet-dump and dotnet-monitor alongside the runtime, and check the .NET 11 diagnostics release notes for new analysis commands.

12. Try it yourself

  1. Run the code example's leaky console app and note its PID.
  2. Run dotnet-dump collect -p <pid> --type Heap -o a.dmp, wait 30 seconds, and collect b.dmp.
  3. In each, run dumpheap -stat and compare the System.Byte[] counts.
  4. Use dumpheap -type System.Byte[] -short, then gcroot on one address to find the static field.
  5. Run clrstack -all and threadpool to see what threads are doing.
  6. Open b.dmp in Visual Studio, Debug with Managed Only, and compare the experience.
  7. Collect dotnet-gcdump collect -p <pid> and compare its size with the heap dump.

💻 Code

A small app that leaks through a static cache, to practise on:

using System.Collections.Concurrent;

Console.WriteLine($"PID {Environment.ProcessId}: run dotnet-dump collect -p {Environment.ProcessId} --type Heap");

var requestId = 0;
while (true)
{
    var key = $"report?customer={Random.Shared.Next()}&page={requestId++}";
    LeakyCache.GetOrAdd(key);
    if (requestId % 10_000 == 0)
    {
        Console.WriteLine($"{requestId} requests, {GC.GetTotalMemory(false) / 1_048_576} MB");
    }
    await Task.Delay(1);
}

public static class LeakyCache
{
    // Unbounded, keyed by raw query strings: grows forever.
    private static readonly ConcurrentDictionary<string, byte[]> Entries = new();

    public static byte[] GetOrAdd(string key) => Entries.GetOrAdd(key, _ => new byte[4_096]);
}

Capture and analyse:

dotnet tool install --global dotnet-dump
dotnet-dump collect -p 12345 --type Heap -o ./dumps/api-1.dmp
dotnet-dump analyze ./dumps/api-1.dmp
> dumpheap -stat
> dumpheap -type System.Collections.Concurrent.ConcurrentDictionary -short
> gcroot 00007f3a2c01d2a8
> clrstack -all
> pe

Automatic crash dumps (container or App Service environment variables):

DOTNET_DbgEnableMiniDump=1
DOTNET_DbgMiniDumpType=4
DOTNET_DbgMiniDumpName=/home/dumps/%e.%p.dmp

The fixed cache with a size limit and expiry (Microsoft.Extensions.Caching.Memory):

using Microsoft.Extensions.Caching.Memory;

var cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 10_000 });

byte[] GetReport(string normalisedKey) =>
    cache.GetOrCreate(normalisedKey, entry =>
    {
        entry.Size = 1;
        entry.SlidingExpiration = TimeSpan.FromMinutes(10);
        return new byte[4_096];
    })!;

Console.WriteLine(GetReport("report:customer:42:page:1").Length);

⚠ Common mistakes

  • Mistake: restarting the process before capturing anything. The restart clears the hang or the leak and destroys the evidence, so the problem returns with nothing learnt. Instead: capture first, restart second. Have the capture steps in the runbook so on-call staff can do it in minutes.

  • Mistake: taking a single dump of a hang. One snapshot can't tell a stuck thread from a busy one that happened to be at that line. Instead: take two dumps a few seconds apart and compare the stacks.

  • Mistake: using a mini dump for a leak. A mini dump doesn't contain the managed heap, so dumpheap and gcroot have nothing to read. Instead: use --type Heap or Full, or dotnet-gcdump when types and references are enough.

  • Mistake: comparing only one leak dump. A big heap at one moment may be normal; what matters is what grows. Instead: take two dumps at different memory sizes and compare dumpheap -stat output.

  • Mistake: analysing without matching PDBs or runtime. Stacks show addresses instead of method names and lines, or the tool can't read runtime structures. Instead: keep PDBs for every release, record the runtime version, and let dotnet-dump download the matching DAC and symbols.

  • Mistake: taking a full dump of a large process at peak without warning anyone. Collection pauses the process for seconds, can fail health checks and can fill the disk. Instead: tell the team, take it from one instance (out of the load balancer if possible), and check disk space first.

  • Mistake: expecting syncblk to show every lock. It lists only contended Monitor locks, not System.Threading.Lock, SemaphoreSlim or async waits. Instead: read clrstack -all and dumpasync as well.

  • Mistake: disabling diagnostics in production images. With DOTNET_EnableDiagnostics=0, dotnet-dump and dotnet-monitor can't connect when you need them. Instead: keep diagnostics enabled and control access to the process and the diagnostics socket instead.

  • Mistake: attaching dumps to tickets, chat or email. A dump can contain tokens, connection strings and customer records. Instead: store dumps encrypted with restricted access and short retention, and share the findings, not the file.

  • Mistake: exposing dotnet-monitor without authentication. Anyone who can reach it can download dumps of your process. Instead: require authentication, keep it on an internal network, and never expose it publicly.

🏛 Senior insight: trade-offs

Seniors prepare for dumps before the incident. They decide in advance how dumps are captured in each hosting platform, who may capture them, where they go (encrypted, in-region storage with short retention) and how PDBs are kept for each release, and they write that into the runbook. During an incident they capture first and restart second, take paired dumps for hangs and two-point dumps for leaks, and then turn the finding into a guard: a bounded cache, a lifetime fix, a memory alert in Application Insights, a load test that checks steady-state memory. They can also explain the cost: a dump briefly freezes the process, so on a multi-instance service they take it from one instance after removing it from the load balancer where possible.

🔗 Follow-ups the interviewer may ask

  1. When: The dump shows threads blocked on locks. Why: A hang found in a dump usually needs a deadlock explanation and fix.
    How to answer

    When it hangs, I press Break All so every thread stops where it is. In the Threads window I look for threads sitting in Monitor.Enter, Lock.Enter or Task.Wait, and switch to each one to read its call stack. Parallel Stacks shows all thread stacks merged into one graph, so two threads each holding a lock and waiting for the other's lock stand out immediately. That's the classic opposite-order deadlock. In production I can't attach, so I take a dump with dotnet-dump and run clrstack -all and syncblk. The fix is a consistent lock order, smaller locks, or removing sync-over-async.

  2. When: The candidate talks about objects, addresses or raw memory in a dump. Why: Checks the candidate understands the object layout they see in a dump.
    How to answer

    The Memory window shows the raw bytes at an address in the paused process, as hex with a text column. In .NET I use it when the bytes themselves are the question: a binary protocol parsed wrongly because of endianness, a UTF-8 versus UTF-16 encoding bug, a struct layout passed to native code, or a buffer in unsafe code. I type a pointer or address expression to jump there. It is not how you find memory leaks. For those I use the Memory Usage tool's heap snapshots and diffs, .NET Object Allocation Tracking, or dotnet-gcdump and dotnet-dump in production.

🛠 Real-world problems: how you handle them

Memory growth to 3 GB from an unbounded static cache
Situation
An ASP.NET Core reporting API on Azure App Service (Linux, Australia East) grew from about 400 MB to over 3 GB during each business day. Operations had set a nightly recycle, and instances occasionally ran out of memory during peaks.
Signals
Application Insights showed working set climbing steadily with traffic and never dropping after gen 2 collections; dotnet-counters showed the GC heap size growing and frequent gen 2 collections; latency spikes coincided with full GCs.
Investigation
Took a heap dump at around 800 MB and another six hours later at 2.5 GB with dotnet-dump. `dumpheap -stat` showed millions of cache entries and `byte[]` instances growing between the two; `gcroot` on samples led to a static cache dictionary in a report service keyed by raw query strings, including a page number and random tracking parameters.
Resolution
Replaced the static dictionary with `MemoryCache` configured with a `SizeLimit`, sliding expiration and normalised keys that ignore tracking parameters. Memory stabilised at around 600 MB and the nightly recycle was removed.
Prevention
Added an Application Insights alert on private bytes, a soak test in the release pipeline that checks steady-state memory, a code review rule against unbounded static collections, and a runbook for collecting and securely storing dumps.

How to tell it (STAR)

**Situation:** Our [reporting API] grew to over 3 GB every day and we were relying on a nightly restart. **Task:** I wanted to remove the root cause instead of the workaround. **Action:** I took two heap dumps hours apart, compared them with `dumpheap -stat`, and used `gcroot` to trace the growing objects to an unbounded static cache keyed by raw query strings. I replaced it with a size-limited `MemoryCache` with normalised keys, and added a memory alert and a soak test. **Result:** Memory levelled off at about [600 MB] and we removed the nightly recycle.

↩ Usually asked after

More in Debugging Tools

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me