Home / Guides / Email when a Windows service stops

Get an email when a Windows service stops

A service stops at 2 a.m. and you hear about it from the first user at 8. Windows has the parts to tell you sooner, an event log, Task Scheduler and PowerShell, but no single switch that says "mail me when this service stops". This guide covers which events Windows writes when a service crashes or stops, a scheduled task that runs a script on those events, a polling task for the stops that leave no event at all, why Send-MailMessage is marked obsolete and what to use instead, how the Recovery tab fits in, and where this approach stops. Every step was run on a Windows 11 test machine with a test service.

Short answer.

When a service crashes, the Service Control Manager writes event 7034 to the System log ("terminated unexpectedly"), or 7031 when Recovery actions are set. A task created with schtasks /create /sc ONEVENT /ec System /mo "<XPath>" runs a PowerShell script within seconds of that event, and the script sends the mail.

When a service is stopped cleanly (a person, net stop, an installer, a service that ends itself without an error), our Windows 11 test machine wrote no event at all; the "entered the stopped state" event 7036 that older articles rely on never appeared. For those stops, run the script every few minutes and compare each service's state with the last run. Microsoft marks Send-MailMessage obsolete because it cannot guarantee a secure connection: send to a relay you control, or use Microsoft Graph's Send-MgUserMail if your mail is in Exchange Online.

What Windows writes when a service stops

These events come from the source Service Control Manager in the System log. The texts are the event templates on Windows 11; the last column is what we saw on our test machine (Windows 11, build 26300) with a test service:

EventTextWhen we saw it
7034The %1 service terminated unexpectedly. It has done this %2 time(s).The service process was killed, no Recovery actions set
7031The %1 service terminated unexpectedly. It has done this %2 time(s). The following corrective action will be taken in %3 milliseconds: %5.The same crash with Recovery set to restart; the service was running again 5 seconds later
7023, 7024The %1 service terminated with the following error (or service-specific error): %2Not triggered in our test; written when a service stops with an error code
7036The %1 service entered the %2 state.Never: not for the clean stop, not for the starts. Over 5,000 Service Control Manager events on a second Windows 11 PC held no 7036 either

Microsoft's own troubleshooting articles still show 7036 next to 7034 (for example for an unexpected SQL Server shutdown), and the event is still defined, so older Windows and Windows Server may well write it. Check your own servers before you build on it:

# Has this server written 7036 ("entered the stopped state") in the last 7 days?
Get-WinEvent -FilterHashtable @{ LogName = 'System'; ProviderName = 'Service Control Manager'; Id = 7036
                                 StartTime = (Get-Date).AddDays(-7) } -MaxEvents 5

# What it did write, by event ID
Get-WinEvent -FilterHashtable @{ LogName = 'System'; ProviderName = 'Service Control Manager' } -MaxEvents 5000 |
    Group-Object Id | Sort-Object Count -Descending | Select-Object Count, Name

If the first command returns nothing for 7036 while services have been started and stopped, your Windows does not log it either, and only the polling task below sees a clean stop. The NoMatchingEventsFound error from Get-WinEvent just means no event matched.

The script: one alert script for both triggers

This script has two modes. Event mode is run by the event-triggered task: it reads the Service Control Manager crash events of the last two minutes and sends them in one mail, and it remembers the last event it reported so two triggers close together do not send the same event twice. Poll mode is run every few minutes: it checks the services you name and sends a mail when one of them is no longer running, once per change, not on every run. -OutFolder writes the mail to a text file instead, which is how we tested it without a mail server.

<#
  Send-ServiceAlert.ps1 - email when a Windows service crashes or stops.
    -Mode Event : run by a task that triggers on Service Control Manager events; reports the events of the last few minutes.
    -Mode Poll  : run every few minutes; reports each watched service that is no longer running (once per change).
  -OutFolder writes the message to a text file instead of sending it, for testing.
#>
param(
    [ValidateSet('Event', 'Poll')][string]$Mode = 'Event',
    [string[]]$Watch          = @(),                      # Poll mode: service names (the Name column of Get-Service)
    [int]$LookBackMinutes     = 2,
    [string]$SmtpServer       = 'smtp.contoso.local',     # your internal relay
    [int]$Port                = 25,
    [switch]$UseSsl,
    [string]$From             = 'alerts@contoso.com',
    [string[]]$To             = @('ops@contoso.com'),
    [string]$OutFolder        = ''
)
$ErrorActionPreference = 'Stop'
$lines = [System.Collections.Generic.List[string]]::new()

if ($Mode -eq 'Event') {
    # 7031/7034: terminated unexpectedly; 7023/7024: terminated with an error; 7036 only if your Windows still logs it
    $filter = @{ LogName = 'System'; ProviderName = 'Service Control Manager'; Id = 7023, 7024, 7031, 7034, 7036
                 StartTime = (Get-Date).AddMinutes(-$LookBackMinutes) }
    try   { $events = @(Get-WinEvent -FilterHashtable $filter) }
    catch { if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { $events = @() } else { throw } }
    # Remember the last event reported, so two triggers close together do not send the same event twice
    $lastFile = Join-Path $PSScriptRoot 'last-event.txt'
    $lastId   = if (Test-Path $lastFile) { [long](Get-Content $lastFile -TotalCount 1) } else { 0 }
    foreach ($e in $events | Where-Object RecordId -gt $lastId | Sort-Object RecordId) {
        if ($e.Id -eq 7036 -and $e.Message -notmatch 'stopped') { continue }   # 7036 is also written for "running"
        $lines.Add(('{0:yyyy-MM-dd HH:mm:ss}  event {1}: {2}' -f $e.TimeCreated, $e.Id, ($e.Message -replace '\s+', ' ').Trim()))
    }
    if ($events.Count -gt 0) { ($events | Measure-Object RecordId -Maximum).Maximum | Set-Content -Path $lastFile }
}
else {
    $stateFile = Join-Path $PSScriptRoot 'service-state.json'
    $last = @{}
    if (Test-Path $stateFile) {
        (Get-Content $stateFile -Raw | ConvertFrom-Json).PSObject.Properties | ForEach-Object { $last[$_.Name] = $_.Value }
    }
    $now = @{}
    foreach ($name in $Watch) {
        $svc    = Get-Service -Name $name -ErrorAction SilentlyContinue
        $status = if ($svc) { [string]$svc.Status } else { 'NotFound' }
        $now[$name] = $status
        if ($status -ne 'Running' -and $last[$name] -ne $status) {             # alert once per change, not on every run
            $label = if ($svc) { "$($svc.DisplayName) ($name)" } else { $name }
            $lines.Add(('{0:yyyy-MM-dd HH:mm:ss}  {1} is {2}' -f (Get-Date), $label, $status))
        }
    }
    $now | ConvertTo-Json | Set-Content -Path $stateFile -Encoding UTF8
}

if ($lines.Count -eq 0) { return }
$subject = "Service alert on $env:COMPUTERNAME"
$body    = ($lines -join "`r`n")

if ($OutFolder) {
    $file = Join-Path $OutFolder ('alert-{0:yyyyMMdd-HHmmss-fff}.txt' -f (Get-Date))
    Set-Content -Path $file -Value "Subject: $subject`r`n`r`n$body" -Encoding UTF8
    return
}
# Send-MailMessage is obsolete (it cannot guarantee a secure connection): use it only towards a relay you control.
Send-MailMessage -SmtpServer $SmtpServer -Port $Port -UseSsl:$UseSsl -From $From -To $To -Subject $subject -Body $body

Save it as C:\Scripts\Send-ServiceAlert.ps1 and set your relay, sender and recipients in the param block. The script keeps two small state files next to itself (last-event.txt and service-state.json), so the account the task runs as needs write access to that folder.

Run it when a service crashes: a task on the event

From an elevated PowerShell prompt, create a task that Task Scheduler starts whenever one of those events is written to the System log. The /mo value is an XPath query on the event, the same kind Event Viewer builds in a custom view:

# Elevated PowerShell. Runs the script whenever a service crashes or ends with an error.
$xpath = "*[System[Provider[@Name='Service Control Manager'] and (EventID=7023 or EventID=7024 or EventID=7031 or EventID=7034 or EventID=7036)]]"
$run   = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Send-ServiceAlert.ps1"'
schtasks.exe /create /tn "Service Alert (event)" /sc ONEVENT /ec System /mo $xpath /tr $run /ru SYSTEM /rl HIGHEST /f

# Test it without mail: write the message to a folder instead
New-Item -ItemType Directory -Force C:\Scripts\outbox | Out-Null
C:\Scripts\Send-ServiceAlert.ps1 -LookBackMinutes 1440 -OutFolder C:\Scripts\outbox

# Remove it again
schtasks.exe /delete /tn "Service Alert (event)" /f

The task runs as SYSTEM, so no password is stored, and SYSTEM can read the System log. Before you rely on it, run the script once by hand with -OutFolder C:\Scripts\outbox to check the text, and once without it to check that the mail arrives.

Catch clean stops: poll every few minutes

A stop that Windows treats as normal leaves no event to trigger on (on our test machine), so check the state on a schedule instead. Name the services with their service name (the Name column of Get-Service, not the display name):

# Elevated PowerShell. Checks the named services every 5 minutes.
$run = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Send-ServiceAlert.ps1" -Mode Poll -Watch Spooler,MyService'
schtasks.exe /create /tn "Service Alert (poll)" /sc MINUTE /mo 5 /tr $run /ru SYSTEM /rl HIGHEST /f

# Remove it again
schtasks.exe /delete /tn "Service Alert (poll)" /f

A stopped service is then reported within five minutes. If the service is started again and stops again, you get a new mail; while it stays stopped, you get one. A service that is uninstalled or misspelled is reported as NotFound.

Tested on Windows 11 (build 26300) on a test VM, with a small test service built for the purpose, no mail server and no credentials, using -OutFolder instead of sending. Both tasks were created with the commands above. A clean Stop-Service wrote no event and did not trigger the event task; the next poll reported it, and a second poll with no change sent nothing. Killing the service process wrote 7034 and the event task wrote its mail within seconds. With Recovery set to restart, a kill wrote 7031, the service was restarted, and the task reported only the new event. A net stop with Recovery set was neither restarted nor logged. The service and both tasks were removed afterwards. Send-MailMessage itself was not run; its parameters are from Microsoft's reference page.

Sending the mail: Send-MailMessage is obsolete

Microsoft's reference page for Send-MailMessage carries a warning: the cmdlet is obsolete because it "doesn't guarantee secure connections to SMTP servers", and Microsoft recommends not using it, while also saying there is no immediate replacement in PowerShell. It still works in Windows PowerShell 5.1 and PowerShell 7. Your options, honestly:

  • An internal relay you control. Keep Send-MailMessage, as the script does, but only towards an SMTP relay on your own network that accepts mail from your servers without a password, and add -UseSsl when the relay supports it. Do not point it at a public mail service with a password in the script.
  • Exchange Online. Microsoft points to Send-MgUserMail in the Microsoft Graph PowerShell SDK. It needs an app registration with the Mail.Send permission and a certificate for unattended sign-in, so it is more setup, but no SMTP password sits on the server. Replace the last line of the script with that call.
  • MailKit. The .NET compatibility note behind the warning (DE0005) suggests the open-source MailKit library for SMTP from code; from PowerShell that means loading its DLLs yourself.
  • No mail at all. If you already have a monitoring system or a chat channel with an incoming webhook, sending the alert there with Invoke-RestMethod is often simpler than mail.

Never put a mail password in the script or in the task's command line: anyone who can read the file or the task can read it.

Where the Recovery tab fits in

The Recovery tab of a service (or sc.exe failure) restarts a service when it fails, that is when its process ends without reporting a stop. That is the same situation that writes 7034 or 7031, so the two work together: Recovery restarts the service, and the event task tells you it happened. Recovery does nothing for a clean stop, a stop by a person, or a hung service; in our test a net stop left the service stopped although Recovery was set. Its Run a program action can also send a mail, but it only runs on a failure, so it covers less than the tasks above. Our guide Restart a Windows service automatically: the Recovery tab vs a watchdog covers the Recovery settings and their gaps in detail.

Where this approach stops

  • It tells you, it does not fix. These tasks send a mail; starting the service again is up to the Recovery tab (crashes only) or a person. Add a Start-Service to the poll and you are writing a watchdog, with the questions the recovery guide describes.
  • A dead server sends nothing. If the whole server is down, rebooting or has lost its network, no task runs and no mail leaves. Something outside the server has to notice that.
  • Up to five minutes late for clean stops, and blind to a service that is running but hung: its state is still Running.
  • One setup per server. The script, both tasks and the list of services live on each server. Twenty servers are twenty copies to keep in step, usually through Group Policy or your deployment tool.
  • A failed send is easy to miss. If the relay is down, the task's Last Run Result shows the error, but nobody looks there at 2 a.m. The event that failed to send is not sent again.
  • Text, not structure. The 7036 check in the script looks for the English word "stopped", which differs on Windows in other languages.

Questions

Which event ID means a Windows service stopped?

In the System log, from the source Service Control Manager: 7034 means the service terminated unexpectedly, 7031 the same with a Recovery action that will be taken, and 7023 or 7024 that it stopped with an error. 7036 ("entered the stopped state") is the classic event for any stop, but on the Windows 11 machines we tested it was not written at all, so check your own servers first.

How do I send an email when a service stops?

Create a scheduled task with an event trigger on the System log (schtasks /create /sc ONEVENT /ec System with an XPath query for the Service Control Manager events) that runs a PowerShell script, and add a second task that checks the service state every few minutes for clean stops, which leave no event. The script sends the mail to your relay.

Why is Send-MailMessage obsolete?

Microsoft's documentation says it does not guarantee secure connections to SMTP servers and recommends not using it, while noting there is no immediate replacement in PowerShell. It still works. Use it only towards a relay you control, or send through Microsoft Graph (Send-MgUserMail) for Exchange Online, or use the MailKit library.

Does the Recovery tab send an alert?

Not by itself. Its Run a program action can start a script that sends a mail, but only when the service fails (its process ends without reporting a stop). A clean stop or a stop by a person runs no Recovery action at all.

Why did my event-triggered task not run when I stopped the service?

A normal stop may write no event. On our Windows 11 test machine Stop-Service and net stop wrote nothing to the System log, so an event trigger cannot fire; only a crash (7034 or 7031) did. Use a task that polls the service state every few minutes for those stops.

Is there a tool that restarts the service and emails me?

SvcSentinel checks the services and programs you choose every 30 seconds, starts any monitored service it finds stopped, for any reason, and emails you through your own SMTP server when it did.

Related

Sources (Microsoft Learn): Send-MailMessage (obsolete warning), Platform compatibility note DE0005, Send-MgUserMail, schtasks create (ONEVENT), Get-WinEvent, Get-WinEvent queries with FilterHashtable, Troubleshoot availability group failover (events 7034 and 7036), Service state transitions, SERVICE_FAILURE_ACTIONS.

An unhandled error has occurred. 🗙