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 the process was to kill it from Task Manager. I knew the automation should only typically take a 5-6 seconds per run, so I wanted the script to time itself out after 30 seconds.

Surprisingly, I’ve 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 that 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 what came back, and call Kill() if it’s false.

The timeout doesn’t kill anything on its own, which caught me out. Returning false just means it stopped waiting, and the process is still sitting there running. Killing it is a separate thing you have to write. Kill() also only sends the signal, so calling WaitForExit() right after is what confirms it’s actually gone, and Close() releases the handle.

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 worth knowing there are two versions of WaitForExit, one that takes a parameter and one that doesn’t. What the parameter version gives you is a decision point. With WaitForExit() your script hands control to the process and doesn’t get it back until the process decides to exit, so if it hangs, your script hangs with it and stays hung. With WaitForExit(30000) you get control back after 30 seconds either way. The process is still running at that point and nothing interrupted it, you just stopped waiting. That’s where you decide what happens next, and killing it is only one option. You could log it, retry it, or skip it and carry on.

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, which is the behaviour I was getting. With WaitForExit(30000), a normal 5 second run returns true after 5 seconds and lands in the else branch, and a hang gives you control back after 30 seconds so the rest of the script keeps going.

The no-argument WaitForExit() still has its place, like right after Kill(), where you aren’t setting a limit so much as confirming the process is gone. The one to avoid is using it for the timeout case itself, where you need the way out.

The thing I never worked out is why the process was hanging in the first place. I put a timeout on it, the pipeline stopped stalling, and I moved on. A 30 second limit on something that normally takes 5 or 6 seconds means I’ll never see a slow run that isn’t a hang, so if it starts taking 20 seconds regularly I won’t find out from this script.