By Chandler Gray• Published: • 4 min read

How to Timeout an External Process in PowerShell

I was troubleshooting a PowerShell script that was part of an automation pipeline today. For the most part, everything ran fine, but I noticed one instance where the process would hang. The only way I was able to stop it was to kill it from Task Manager. I knew the automation typically takes 5-6 seconds per run, so I wanted the script to time itself out after 30 seconds.

Surprisingly, I’d never had to do anything like this before. After a little research, I found the WaitForExit(Int32) method, which I thought was perfect.

WaitForExit(Int32) is a method on the System.Diagnostics.Process object. It blocks the current thread for up to the number of milliseconds you give it, then returns true if the process exited in that window or false if it didn’t. In PowerShell, you get the process object by passing -PassThru to Start-Process, which returns the object instead of discarding it. From there you call WaitForExit(30000) for a 30-second limit, check the return value, and call Kill() if it came back false.

The process isn’t killed automatically when the timeout runs out, so that step has to be written out. Microsoft’s documentation for Kill() says it “executes asynchronously,” so calling WaitForExit() right after makes sure the process has fully stopped before the script continues. Calling Close() at the end frees the resources held for the exited process.

Here’s an example of how I used it:

$proc = Start-Process python -ArgumentList "automation.py" -PassThru

if (-not $proc.WaitForExit(30000)) {
    $proc.Kill()
    $proc.WaitForExit()
    Write-Host "Timed out. Process killed."
} else {
    Write-Host "Done. Exit code: $($proc.ExitCode)"
}

$proc.Close()

It’s important to note that there are two versions of WaitForExit, one with a parameter and one without. What WaitForExit(Int32) gives you that the no-argument version doesn’t is a decision point. With WaitForExit(), your script hands control to the process and doesn’t get it back until the process exits, which the documentation describes as waiting “indefinitely.” If the process hangs, your script hangs with it. With WaitForExit(30000), you get control back after 30 seconds regardless. The process is still running at that point. Nothing interrupted it, you just stopped waiting for it. That’s when you decide what to do next. In this case, I kill it, but you could also log it, retry it, or skip it.

To make this concrete, say your script normally finishes in 5 seconds but occasionally gets stuck. With WaitForExit():

$proc = Start-Process python -ArgumentList "automation.py" -PassThru
$proc.WaitForExit()
Write-Host "Done."

If the script hangs, Write-Host "Done." never runs, and your pipeline stops there. With WaitForExit(30000), a normal 5-second run returns true after 5 seconds and lands in the else branch. If it hangs, you get control back after 30 seconds, kill it, and the rest of your script continues.

The no-argument WaitForExit() still has its place, like right after Kill(), where you aren’t setting a time limit but confirming the process is gone. What you want to avoid is using it for the timeout itself, where you need a way out if the process hangs.

One more detail from the Kill() documentation. Kill() stops only the process you started, not any processes it started in turn. On .NET Core 3.0 and later, there’s a Kill(true) overload that also stops the process’s descendants. The same page notes that WaitForExit() only reflects the process itself, not its descendants, even when you use Kill(true).