ZetCode

C# Process

last modified October 18, 2026

In this article, we explore how to work with processes in C# using the System.Diagnostics namespace.

The Process class provides access to local processes, allowing developers to start, stop, monitor, and interact with system processes. Whether it's launching an external application, capturing process output, or managing running tasks, the Process class offers a comprehensive set of functionalities.

To configure process execution, C# provides the ProcessStartInfo class, which specifies various settings such as:

The examples run on Linux and macOS; on Windows, some of the commands, such as sleep, are not available and must be replaced.

C# Process simple example

In the first example, we start a command and wait until it finishes.

Program.cs
using System.Diagnostics;

using var process = Process.Start("dotnet", "--version");
process.WaitForExit();

Console.WriteLine($"exit code: {process.ExitCode}");

The example runs the dotnet --version command.

using var process = Process.Start("dotnet", "--version");

The Start method starts a process with the given program and arguments. The program is looked up in the directories of the PATH environment variable. The returned Process object is disposable.

process.WaitForExit();

Console.WriteLine($"exit code: {process.ExitCode}");

The WaitForExit method waits until the process ends. After that, the ExitCode property contains the value the process returned; by convention, zero means success. The output of the child process goes directly to our console.

$ dotnet run
10.0.203
exit code: 0

C# Process capture output

The StandardOutput property gets a stream used to read the textual output of the process. To use it, the output must be redirected.

Program.cs
using System.Diagnostics;

var psi = new ProcessStartInfo("dotnet")
{
    RedirectStandardOutput = true,
    UseShellExecute = false
};

psi.ArgumentList.Add("--list-sdks");

using var process = Process.Start(psi)!;

string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();

string[] lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);

Console.WriteLine($"exit code: {process.ExitCode}");
Console.WriteLine($"{lines.Length} SDK(s) installed:");

foreach (var line in lines)
{
    Console.WriteLine($"  {line.Split(' ')[0]}");
}

We list the installed .NET SDKs and process the output of the command.

var psi = new ProcessStartInfo("dotnet")
{
    RedirectStandardOutput = true,
    UseShellExecute = false
};

Redirection requires UseShellExecute to be false, which is the default in .NET (Core); in .NET Framework, the default was true. In this context, the shell refers to the operating system shell that opens documents and URLs, not to command shells such as bash.

psi.ArgumentList.Add("--list-sdks");

The ArgumentList collection takes each argument separately and quotes it correctly, so arguments with spaces or quotes are passed safely. It is preferred over building the Arguments string by hand.

string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();

We read the whole output and wait for the process to exit. The output must be read before waiting; if the process fills the pipe buffer while nobody reads it, it blocks and never exits.

$ dotnet run
exit code: 0
1 SDK(s) installed:
  10.0.203

C# Process standard error

A process writes error messages to the standard error stream. When we redirect both streams, we must read them concurrently; reading one stream to its end while the process is blocked writing to the other one causes a deadlock.

Program.cs
using System.Diagnostics;

var result = await RunAsync("git", "--version");
Print(result);

result = await RunAsync("git", "no-such-command");
Print(result);

static async Task<(int ExitCode, string Output, string Error)> RunAsync(string file, params string[] args)
{
    var psi = new ProcessStartInfo(file)
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true
    };

    foreach (var arg in args)
    {
        psi.ArgumentList.Add(arg);
    }

    using var process = Process.Start(psi)!;

    Task<string> outTask = process.StandardOutput.ReadToEndAsync();
    Task<string> errTask = process.StandardError.ReadToEndAsync();

    await process.WaitForExitAsync();

    return (process.ExitCode, await outTask, await errTask);
}

static void Print((int ExitCode, string Output, string Error) r)
{
    Console.WriteLine($"exit code: {r.ExitCode}");
    Console.WriteLine($"stdout: {r.Output.Trim()}");
    Console.WriteLine($"stderr: {r.Error.Trim()}");
    Console.WriteLine();
}

The RunAsync helper runs a command and returns the exit code together with the output and the error output.

Task<string> outTask = process.StandardOutput.ReadToEndAsync();
Task<string> errTask = process.StandardError.ReadToEndAsync();

await process.WaitForExitAsync();

return (process.ExitCode, await outTask, await errTask);

We start reading both streams before we wait, so that they are read at the same time.

$ dotnet run
exit code: 0
stdout: git version 2.34.1
stderr:

exit code: 1
stdout:
stderr: git: 'no-such-command' is not a git command. See 'git --help'.

The second command fails; the exit code is 1 and the message is in the error output.

C# Process standard input

With RedirectStandardInput, we can write data to the process.

Program.cs
using System.Diagnostics;

var psi = new ProcessStartInfo("sort")
{
    RedirectStandardInput = true,
    RedirectStandardOutput = true
};

using var process = Process.Start(psi)!;

string[] words = ["sky", "cloud", "falcon", "owl", "crane", "atom"];

foreach (var word in words)
{
    await process.StandardInput.WriteLineAsync(word);
}

process.StandardInput.Close();

string sorted = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();

Console.Write(sorted);

We send words to the sort command and read them back sorted.

process.StandardInput.Close();

The sort command reads its input until the end of the stream before it prints anything. Closing the input stream signals the end.

$ dotnet run
atom
cloud
crane
falcon
owl
sky

C# Process timeout and kill

The next example waits for a process with a timeout and kills it if it runs too long.

Program.cs
using System.Diagnostics;

using var process = Process.Start("sleep", "30");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));

var sw = Stopwatch.StartNew();

try
{
    await process.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine($"timed out after {sw.Elapsed.TotalSeconds:F1} s, killing process {process.Id}");
    process.Kill(entireProcessTree: true);
    await process.WaitForExitAsync();
}

Console.WriteLine($"has exited: {process.HasExited}, exit code: {process.ExitCode}");

We start the sleep 30 command and give it two seconds.

await process.WaitForExitAsync(cts.Token);

When the token is cancelled, WaitForExitAsync throws an OperationCanceledException. Cancelling the wait does not stop the process itself.

process.Kill(entireProcessTree: true);
await process.WaitForExitAsync();

The Kill method terminates the process; with entireProcessTree, it also terminates the processes started by it. Kill does not wait, so we wait for the exit afterwards.

$ dotnet run
timed out after 2.0 s, killing process 4708
has exited: True, exit code: 137

On Linux, a process killed with the SIGKILL signal reports the exit code 137 (128 + 9).

C# open file or URL

With UseShellExecute set to true, the operating system opens a document or a URL with its default application, as if the user double-clicked it.

Process.Start(new ProcessStartInfo("https://zetcode.com") { UseShellExecute = true });
Process.Start(new ProcessStartInfo("report.pdf") { UseShellExecute = true });

The first line opens the URL in the default browser, the second opens the PDF file in the default viewer. On Linux, this requires a desktop environment with the xdg-open command.

C# Process.GetProcessesByName

The Process.GetProcessesByName method returns the running processes that have the specified name.

Program.cs
using System.Diagnostics;

Process[] processes = Process.GetProcessesByName("dotnet");
Console.WriteLine($"{processes.Length} dotnet process(es)");

foreach (var process in processes)
{
    using (process)
    {
        Console.WriteLine($"Id: {process.Id}, memory: {process.WorkingSet64 / 1048576} MB");
    }
}

We find the dotnet processes and print their ids and memory usage. The name is given without the .exe extension.

foreach (var process in processes)
{
    using (process)
    {
        ...
    }
}

Each returned Process object holds operating system resources, so we dispose it.

$ dotnet run
1 dotnet process(es)
Id: 16961, memory: 91 MB

C# Process.GetProcesses

The Process.GetProcesses method returns all processes running on the local computer.

Program.cs
using System.Diagnostics;

Process[] processes = Process.GetProcesses();

var top = processes
    .OrderByDescending(p => p.WorkingSet64)
    .Take(5);

Console.WriteLine($"{processes.Length} processes, top 5 by memory:");

foreach (var p in top)
{
    string name = p.ProcessName.Length > 20 ? p.ProcessName[..20] : p.ProcessName;
    Console.WriteLine($"{name,-20} {p.Id,8} {p.WorkingSet64 / 1048576,8} MB");
}

foreach (var p in processes)
{
    p.Dispose();
}

We print the five processes that use the most memory. The WorkingSet64 property gives the amount of physical memory used by the process.

string name = p.ProcessName.Length > 20 ? p.ProcessName[..20] : p.ProcessName;

On Linux, some programs change their process name to a long command line, so we shorten the names.

$ dotnet run
325 processes, top 5 by memory:
code                     4147      518 MB
Code --standard-sche     3098      480 MB
kilo                     3479      449 MB
code                     3441      377 MB
code                     3051      313 MB

C# current process

The Process.GetCurrentProcess method returns the process of our own program.

Program.cs
using System.Diagnostics;

using var current = Process.GetCurrentProcess();

Console.WriteLine($"Name: {current.ProcessName}");
Console.WriteLine($"Id: {current.Id}");
Console.WriteLine($"Started: {current.StartTime:HH:mm:ss}");
Console.WriteLine($"Threads: {current.Threads.Count}");
Console.WriteLine($"Memory: {current.WorkingSet64 / 1048576} MB");
Console.WriteLine($"Main module: {Path.GetFileName(current.MainModule?.FileName)}");

We print information about the running program. For the process id alone, Environment.ProcessId is simpler and faster.

$ dotnet run
Name: ProcessEx
Id: 4830
Started: 08:58:25
Threads: 7
Memory: 25 MB
Main module: ProcessEx

Source

Process class - language reference

In this article we have worked with processes in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.