Memory.Introspect

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:

deployment.yaml
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 runAsUser is 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

Referenced by

© 2026 Memory.Introspect. All rights reserved.