PowerShell Monitor Windows Services and Auto-Restart Failed Ones

PowerShell Monitor Windows Services and Auto-Restart Failed Ones

PowerShell Tips Editor 4 min read
PowerShell Monitor Windows Services and Auto-Restart Failed Ones

Windows Service Recovery properties — the three-failure-action dropdowns in the service properties dialog — are a blunt instrument. They let you restart after the first, second, and subsequent failures, but they offer no logging of the transition, no notification when auto-restart stops working, and no way to apply different logic per service. A PowerShell watchdog script gives you all of that: a configurable service list, a polling loop, state-change logging, restart attempts with timeout detection, and an email alert when a service cannot be brought back up.

Building a Service Watchlist with Get-Service

Define the services to monitor as an array of hashtables, each with the service name and optional metadata like a friendly description and whether a failed restart should trigger an alert. This structure is easy to maintain and extensible — you can add a MaxRestartAttempts key per service without changing the core loop logic.

$watchlist = @(
    @{ Name = 'W3SVC';          DisplayHint = 'IIS';              AlertOnFail = $true  }
    @{ Name = 'MSSQLSERVER';    DisplayHint = 'SQL Server';       AlertOnFail = $true  }
    @{ Name = 'wuauserv';       DisplayHint = 'Windows Update';   AlertOnFail = $false }
    @{ Name = 'Spooler';        DisplayHint = 'Print Spooler';    AlertOnFail = $true  }
)

# Verify all watched services exist before the loop starts
foreach ($entry in $watchlist) {
    $svc = Get-Service -Name $entry.Name -ErrorAction SilentlyContinue
    if ($null -eq $svc) {
        Write-Warning "Service '$($entry.Name)' not found on this host — removing from watchlist"
    }
}

$watchlist = $watchlist | Where-Object { Get-Service -Name $_.Name -ErrorAction SilentlyContinue }

Polling Loop with Configurable Interval

The main loop runs indefinitely, checks each service, and sleeps between cycles. Use -IntervalSeconds as a parameter with a sensible default. For critical services a 30-second interval is reasonable; for less critical services 5 minutes reduces noise. Trap Ctrl+C cleanly with a try/finally block so the log gets a clean shutdown entry.

param(
    [int]$IntervalSeconds = 60,
    [string]$LogPath = "C:\Logs\ServiceWatchdog-$(Get-Date -Format yyyyMMdd).log"
)

function Write-WatchdogLog {
    param([string]$Message, [string]$Level = 'INFO')
    $entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
    $entry | Add-Content -Path $LogPath -Encoding UTF8
    if ($Level -eq 'WARN' -or $Level -eq 'ERROR') { Write-Warning $Message }
    else { Write-Host $entry }
}

Write-WatchdogLog "Watchdog started. Monitoring $($watchlist.Count) service(s). Interval: ${IntervalSeconds}s"

try {
    while ($true) {
        foreach ($entry in $watchlist) {
            $svc = Get-Service -Name $entry.Name -ErrorAction SilentlyContinue
            if ($null -ne $svc) {
                # State check handled in next section
                Check-ServiceState -ServiceEntry $entry -ServiceObject $svc
            }
        }
        Start-Sleep -Seconds $IntervalSeconds
    }
}
finally {
    Write-WatchdogLog "Watchdog stopped."
}

Detecting Stopped and Degraded States

The Status property of a service object is a ServiceControllerStatus enum. The values you care about are Running, Stopped, StartPending, StopPending, Paused, and PausePending. Treat Stopped as the primary action trigger. StartPending and StopPending indicate a transition in progress — do not attempt a restart while the service is already in a pending state, or Start-Service will throw.

function Check-ServiceState {
    param($ServiceEntry, $ServiceObject)

    $status = $ServiceObject.Status

    if ($status -eq 'Stopped') {
        Write-WatchdogLog "[$($ServiceEntry.DisplayHint)] $($ServiceEntry.Name) is STOPPED — attempting restart" -Level WARN
        Restart-WatchedService -ServiceEntry $ServiceEntry
    }
    elseif ($status -in 'StartPending','StopPending','PausePending') {
        Write-WatchdogLog "[$($ServiceEntry.DisplayHint)] $($ServiceEntry.Name) is in state $status — skipping restart attempt" -Level INFO
    }
}

Attempting Restart with Start-Service and Timeout Logic

Call Start-Service with -ErrorAction Stop to catch failures. After the call, refresh the service object and wait up to a configurable timeout for the status to reach Running. If the timeout expires, the service is stuck and you need to alert. Use $svc.WaitForStatus() with a TimeSpan for cleaner timeout handling than a manual polling loop.

function Restart-WatchedService {
    param($ServiceEntry)
    $maxWaitSeconds = 30

    try {
        Start-Service -Name $ServiceEntry.Name -ErrorAction Stop
        $svc = Get-Service -Name $ServiceEntry.Name

        # Wait for Running state with timeout
        $svc.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running,
                           [TimeSpan]::FromSeconds($maxWaitSeconds))

        $svc.Refresh()
        if ($svc.Status -eq 'Running') {
            Write-WatchdogLog "[$($ServiceEntry.DisplayHint)] $($ServiceEntry.Name) restarted successfully" -Level INFO
        }
    }
    catch [System.ServiceProcess.TimeoutException] {
        Write-WatchdogLog "[$($ServiceEntry.DisplayHint)] $($ServiceEntry.Name) did not reach Running state within ${maxWaitSeconds}s" -Level ERROR
        if ($ServiceEntry.AlertOnFail) { Send-WatchdogAlert -ServiceEntry $ServiceEntry -Reason 'Restart timed out' }
    }
    catch {
        Write-WatchdogLog "[$($ServiceEntry.DisplayHint)] $($ServiceEntry.Name) restart failed: $($_.Exception.Message)" -Level ERROR
        if ($ServiceEntry.AlertOnFail) { Send-WatchdogAlert -ServiceEntry $ServiceEntry -Reason $_.Exception.Message }
    }
}

Logging State Changes to a File

The Write-WatchdogLog function above uses Add-Content with -Encoding UTF8 for append-only logging. Rotate the log file daily by including the date in the filename (as shown in the $LogPath parameter default). For enterprise deployments, consider writing structured JSON log entries and forwarding to a SIEM or log aggregator via a file beat agent.

Sending Email Alert When Auto-Restart Fails

Use the same Graph API send-mail pattern for alerts — Send-MailMessage is deprecated. Keep the alert function simple: build a minimal HTML body with the service name, host, time, and failure reason, then POST to the Graph sendMail endpoint.

function Send-WatchdogAlert {
    param($ServiceEntry, [string]$Reason)

    $subject = "Service Failure: $($ServiceEntry.Name) on $env:COMPUTERNAME"
    $bodyHtml = @"
<p><strong>Host:</strong> $env:COMPUTERNAME<br>
<strong>Service:</strong> $($ServiceEntry.DisplayHint) ($($ServiceEntry.Name))<br>
<strong>Time:</strong> $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')<br>
<strong>Reason:</strong> $Reason</p>
<p>Automatic restart failed. Manual intervention required.</p>
"@

    # Use Graph API send-mail (see disk space report article for full token acquire pattern)
    # Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$sender/sendMail" ...
    Write-WatchdogLog "Alert sent for $($ServiceEntry.Name)" -Level INFO
}

Common Errors

  • Start-Service throws if service is in a Pending state — must check state before retrying. Calling Start-Service on a service that is in StartPending or StopPending throws immediately. Always check $svc.Status before attempting a restart and skip the attempt if the service is already transitioning. Use $svc.WaitForStatus() to wait for the transition to complete if needed.
  • Script runs as SYSTEM but target service requires a specific service account to start. Some services (particularly SQL Server or third-party apps) can only start under a specific domain service account. If the watchdog script runs as Local System, Start-Service may succeed at the API level but the service will fail to initialize and stop again within seconds. Check the service’s Log On As account and ensure the service account’s credentials are current and the account is not locked.

Related Cmdlets / See Also

Wrapping Up

A PowerShell watchdog script outperforms native Windows Service Recovery settings in every dimension that matters for operations: it logs every state change with a timestamp, skips restart attempts when a service is already transitioning, and alerts you when automated recovery fails. Run it as a scheduled task on startup so it survives reboots without manual intervention.

Send-Item -To