.NET 11 process signals close an awkward gap in System.Diagnostics.Process: a supervisor can now ask a child process to stop with Process.Signal(PosixSignal) and receive a ProcessExitStatus that preserves whether the child exited normally or was terminated by a signal. On Linux and macOS, that makes a bounded SIGTERM-then-SIGKILL shutdown possible without a P/Invoke wrapper or a shell command.
The APIs first appear in .NET 11 RC1, released on September 8, 2026 under a go-live license. They are useful now for teams validating .NET 11, but .NET 11 is still a prerelease at the time of writing. The pattern below is deliberately scoped to supervising one process on Linux or macOS. It does not signal a process group, container, or remote workload.
Table of Contents
Understand the .NET 11 process signals contract
The new surface separates two jobs that older Process code often mixed together:
Signal(PosixSignal)requests a specific transition. It returnstruewhen the signal was sent andfalsewhen the process had already exited.WaitForExitStatus(),TryWaitForExitStatus(...), andWaitForExitStatusAsync(...)wait for termination and return aProcessExitStatus.ProcessExitStatus.ExitCodecarries the conventional exit value.ProcessExitStatus.Signalis populated on Unix when a signal terminated the process; it isnullafter a normal Unix exit and on Windows.ProcessExitStatus.Canceledindicates termination performed by the new timeout-or-cancellation helper paths. It is not a general substitute for inspectingSignal.
This distinction matters operationally. A conventional exit value such as 143 loses whether the program returned 143 itself or the operating system ended it with SIGTERM and a shell translated the result. The status object keeps the signal as a separate fact.
The new status waits also have a subtle stream boundary: unlike the older parameterless WaitForExit(), they do not wait for redirected standard output and standard error to reach end of file. A supervisor must begin draining both streams before it waits, then await those drain tasks separately.
Start the child and drain redirected output
Set up the child with UseShellExecute = false and use ArgumentList rather than concatenating a command line. Start output reads immediately after Start(). That prevents a verbose child from filling an OS pipe while the parent waits for it to exit.
using System.Diagnostics;
using System.Runtime.InteropServices;
using var child = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "/usr/bin/bash",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
child.StartInfo.ArgumentList.Add("./worker.sh");
if (!child.Start())
{
throw new InvalidOperationException("The child process did not start.");
}
Task<string> stdoutTask = child.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = child.StandardError.ReadToEndAsync();
ProcessExitStatus status = await StopGracefullyAsync(
child,
TimeSpan.FromSeconds(10),
cancellationToken);
string[] output = await Task.WhenAll(stdoutTask, stderrTask);
The sample retains the Process object until the wait and both stream drains complete. Disposing it earlier can close handles while asynchronous work is still using them. In a long-running supervisor, keep those tasks with the child’s state record rather than creating fire-and-forget reads.
Request graceful shutdown before escalating
SIGTERM is a request, not a guarantee. A cooperative worker can stop accepting work, finish or abandon in-flight operations according to its policy, flush durable state, and exit. The supervisor should bound that opportunity. If the deadline expires, SIGKILL ends the process without running its shutdown handler.
static async Task<ProcessExitStatus> StopGracefullyAsync(
Process process,
TimeSpan gracePeriod,
CancellationToken cancellationToken)
{
if (!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS())
{
throw new PlatformNotSupportedException(
"This SIGTERM shutdown policy targets Linux and macOS.");
}
if (!process.Signal(PosixSignal.SIGTERM))
{
return await process.WaitForExitStatusAsync(
CancellationToken.None);
}
using var deadline = new CancellationTokenSource(gracePeriod);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
deadline.Token);
try
{
return await process.WaitForExitStatusAsync(linked.Token);
}
catch (OperationCanceledException)
when (deadline.IsCancellationRequested &&
!cancellationToken.IsCancellationRequested)
{
_ = process.Signal(PosixSignal.SIGKILL);
return await process.WaitForExitStatusAsync(
CancellationToken.None);
}
}
The filter distinguishes an expired grace period from caller cancellation. If the caller cancels first, the method propagates OperationCanceledException and does not silently convert that policy decision into a kill. The owner must then decide whether a separate cleanup path should keep waiting, escalate, or transfer responsibility to another supervisor.
The second Signal result is intentionally ignored because the child may exit between the timeout and SIGKILL. Waiting without cancellation is what reaps the process and obtains the final status in either case. Put a higher-level watchdog around the entire supervisor if the operating environment itself can become unhealthy.
Preserve the real termination cause
Do not collapse every outcome into ExitCode. Record the signal and the cancellation flag as structured fields so alerts and retry policy can distinguish a clean stop from a forced one.
if (status.Signal is PosixSignal signal)
{
logger.LogWarning(
"Child {ProcessId} ended by {Signal}; canceled={Canceled}",
child.Id,
signal,
status.Canceled);
}
else
{
logger.LogInformation(
"Child {ProcessId} exited with code {ExitCode}",
child.Id,
status.ExitCode);
}
A SIGTERM after a planned deployment is usually expected. SIGKILL after the grace period deserves a different counter because it means cleanup did not finish. A nonzero normal exit may follow application-specific retry rules. Keeping those paths separate prevents a normal rolling restart from looking like a crash while still making forced termination visible.
Handle races and platform boundaries
Process supervision is a race by definition. The child can exit after a state check and before a signal, after SIGTERM and before the wait begins, or at the same moment the grace deadline expires. Treat Signal returning false as an ordinary already-exited path. Always obtain the final status rather than inventing one from the request you tried to send.
- One PID, not a tree:
Process.Signaltargets the associated process. If that process launches descendants, use an explicit process-group, job-object, container, or service-manager policy. - Unix-first graceful policy: the runtime implementation supports POSIX signals on Unix. On Windows, only SIGKILL is supported and maps to process termination; SIGTERM is not a cross-platform graceful-stop abstraction.
- Mobile platform annotations: the APIs are marked unsupported on iOS and tvOS, with explicit Mac Catalyst support. Keep analyzers enabled and do not suppress those warnings globally.
- Permissions still apply: the operating system can reject signaling a process the caller does not own or cannot control. Run supervisors with the least privileges that still satisfy the deployment model.
- No implicit protocol: SIGTERM only helps when the child handles it or accepts its default behavior. Define what “graceful” means for that worker and make the deadline longer than its documented cleanup budget.
If an application already runs under systemd, Kubernetes, Azure Container Apps, or another orchestrator, prefer that platform’s lifecycle contract for the top-level workload. The direct API is most valuable when a .NET process genuinely owns a local child and must account for that child’s outcome.
Verify the shutdown contract
A useful verification fixture needs observable graceful and forced paths. The following shell worker writes a marker when SIGTERM reaches its handler:
#!/usr/bin/env bash
set -eu
trap 'printf "graceful-stop\n" >&2; exit 0' TERM
while true; do
sleep 1
done
Run the supervisor on a Linux or macOS machine with the .NET 11 RC1 SDK and check four cases. These are validation steps, not results claimed by this article:
- Normal exit: use a child that returns zero and confirm
SignalisnullandExitCodeis zero. - Cooperative SIGTERM: use the trap above, request shutdown, confirm the marker is captured from standard error, and inspect the returned status.
- Escalation: replace the trap with
trap '' TERM, shorten the grace period, and confirm the status records SIGKILL rather than a fabricated timeout exit code. - Already-exited race: let a short-lived child finish before signaling and confirm the method still returns its actual status.
Also exercise continuous output on both streams. The test should fail if either drain task is removed, because a supervisor that works only with quiet children is not production-ready. Assert structured fields—exit code, signal, and whether escalation occurred—rather than matching a single formatted log line.
Use a production readiness checklist
- Target
net11.0and pin the intended prerelease SDK while evaluating RC1. - Gate the SIGTERM policy to Linux and macOS instead of assuming Windows parity.
- Start standard-output and standard-error drains before waiting for exit.
- Use a documented grace period derived from the child’s shutdown work.
- Keep caller cancellation distinct from the grace deadline.
- After escalation, wait for the real final status and retain the
Signalvalue. - Track forced kills separately from planned graceful stops and normal nonzero exits.
- Define ownership for descendants; a single
Processinstance is not a process-tree abstraction. - Test normal exit, handled SIGTERM, ignored SIGTERM, output pressure, and the already-exited race on every supported OS.
.NET 11 process signals remove platform interop from the common single-child shutdown path, but they do not remove the need for a lifecycle policy. The reliable design remains request, wait, escalate, reap, drain, and classify—each step explicit and observable.
References
- Microsoft .NET Blog: .NET 11 Release Candidate 1
- .NET 11 RC1 library release notes
- Approved Process signal and exit-status API proposal
- dotnet/runtime implementation pull request
- ProcessExitStatus runtime source
Found this useful? Support more practical developer content.