Remote diagnostic ports
Memory.Introspect's default behaviour is to talk to the runtime's per-PID diagnostics pipe — fine for self-capture and same-host cross-process capture. For sidecars and managed-from-outside deployments, the .NET runtime supports named diagnostic ports, and Memory.Introspect supports them via the DiagnosticPort option.
The mental model
When a .NET process starts, the runtime always exposes a default pipe at:
- Linux/macOS:
/tmp/dotnet-diagnostic-<pid>-<startup-cookie>-socket - Windows:
\\.\pipe\dotnet-diagnostic-<pid>
If the environment variable DOTNET_DiagnosticPorts is set, the runtime also listens on (or connects out to) each additional port listed. Sidecars use this to attach without needing PID hunting.
Configuring the target process
Set DOTNET_DiagnosticPorts before the target process starts. The simplest form is a fixed path:
DOTNET_DiagnosticPorts="/tmp/dotnet-diagnostic-app" dotnet MyApp.dll
In Kubernetes, surface it via a Deployment env var on the application container:
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: DOTNET_DiagnosticPorts
value: /tmp/diag/dotnet-diag-app
volumeMounts:
- name: diag
mountPath: /tmp/diag
- name: diagnostics-sidecar
image: my-introspect-sidecar:latest
volumeMounts:
- name: diag
mountPath: /tmp/diag
volumes:
- name: diag
emptyDir: {}
Both containers share /tmp/diag — the runtime listens on /tmp/diag/dotnet-diag-app; the sidecar opens the same path.
Connecting from Memory.Introspect
var introspector = MemoryIntrospector.Create(new()
{
Logger = logger,
DiagnosticPort = "/tmp/diag/dotnet-diag-app",
Timeout = TimeSpan.FromMinutes(5),
});
// PID 0 means "use the diagnostic port"
var result = await introspector.CollectMemoryGraphAsync(processId: 0);
When DiagnosticPort is set, the PID argument is ignored — the library routes everything through the named port. You can pass any PID-like value (0 is conventional).
"Suspend" mode
DOTNET_DiagnosticPorts supports a ,suspend qualifier:
DOTNET_DiagnosticPorts="/tmp/diag/dotnet-diag-app,suspend"
That makes the target process wait at startup until something connects to the diagnostic port. Useful for capturing the first few seconds of execution — a sidecar that needs to attach early can do so deterministically.
Memory.Introspect connects on any CollectXxx call, which releases the suspended process. If you want to attach a sampling profile to the very first millisecond of execution, call CollectSamplingProfileAsync from the sidecar's startup path.
Multiple ports
DOTNET_DiagnosticPorts accepts a semicolon-separated list (on Linux/macOS) or a ;-delimited list on Windows:
DOTNET_DiagnosticPorts="/tmp/diag/a;/tmp/diag/b"
The runtime listens on every entry. Different sidecars can attach to different ports without contending.
Permissions
- On Linux/macOS, both processes must have read+write access to the named socket. Sharing a volume between containers with matching
runAsUseris the usual approach. - On Windows, the named pipe inherits the security descriptor of the listening process. Cross-user access requires explicit ACL setup.
When not to use named ports
If both the introspecting code and the target run in the same process, you don't need a named port — just pass Process.GetCurrentProcess().Id to the standard APIs. Named ports are for the multi-process case.
If both processes are on the same host and run as the same user, the per-PID default pipe works fine for cross-process capture. Named ports are only worth the setup when:
- The introspecting process can't discover the target PID at the right moment, or
- You want the capability boundary to be the port (anyone with access to the port can dump) rather than the PID (anyone with access to the user's processes).
Further reading
- .NET docs — Diagnostic Port — the canonical reference for
DOTNET_DiagnosticPorts. dotnet/diagnosticsissue #2331 — discussion of suspend-mode semantics.