Guide
Server Monitoring Alerts with PowerShell Email
Gartner reports the average cost of IT downtime is $5,600 per minute. You don't need Datadog or PagerDuty for basic server monitoring. PowerShell can check disk space, service health, Event Log errors, and scheduled task status -- then email you the moment something breaks. This guide covers each pattern with Nylas CLI. Works with Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP.
By Qasim Muhammad
One-time setup
Install Nylas CLI and authenticate once. See the PowerShell email guide for full install options.
# Install and authenticate (one time)
irm https://cli.nylas.com/install.ps1 | iex
nylas auth config
# Paste your API key from dashboard-v3.nylas.comDisk space alerts
According to Microsoft's Windows Server documentation, running below 10% free disk space causes performance degradation, and below 1% can corrupt the Event Log. This script checks every drive and alerts when free space drops below a threshold.
# disk-alert.ps1 -- Email alert when disk space is low
param(
[int]$ThresholdPercent = 10,
[string]$AlertTo = "ops@company.com"
)
$drives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID,
@{N='SizeGB'; E={[math]::Round($_.Size/1GB, 1)}},
@{N='FreeGB'; E={[math]::Round($_.FreeSpace/1GB, 1)}},
@{N='FreePct'; E={[math]::Round(($_.FreeSpace/$_.Size)*100, 1)}}
$lowDrives = $drives | Where-Object { $_.FreePct -lt $ThresholdPercent }
if ($lowDrives) {
$hostname = $env:COMPUTERNAME
$details = $lowDrives | ForEach-Object {
"$($_.DeviceID) $($_.FreeGB) GB free of $($_.SizeGB) GB ($($_.FreePct)%)"
}
$body = @"
Disk space alert on $hostname
The following drives are below $ThresholdPercent% free:
$($details -join "`n")
All drives:
$($drives | ForEach-Object { "$($_.DeviceID) $($_.FreeGB)/$($_.SizeGB) GB ($($_.FreePct)%)" } | Out-String)
Checked at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"@
nylas email send --to $AlertTo `
--subject "DISK ALERT: $hostname - $($lowDrives.Count) drive(s) low" `
--body $body --yes
Write-Host "Alert sent for $($lowDrives.Count) low drive(s)"
} else {
Write-Host "All drives OK (above $ThresholdPercent% free)"
}Service health checks
Windows servers run dozens of services. If SQL Server, IIS, or your application service stops, you want to know immediately -- not when a customer reports it 20 minutes later.
# service-monitor.ps1 -- Check critical services and alert on failures
param(
[string[]]$CriticalServices = @(
"W3SVC", # IIS
"MSSQLSERVER", # SQL Server
"wuauserv", # Windows Update
"Spooler", # Print Spooler
"WinRM" # Windows Remote Management
),
[string]$AlertTo = "ops@company.com",
[switch]$AttemptRestart
)
$hostname = $env:COMPUTERNAME
$stoppedServices = @()
foreach ($svcName in $CriticalServices) {
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if (-not $svc) {
Write-Host "Service not found: $svcName" -ForegroundColor DarkGray
continue
}
if ($svc.Status -ne 'Running') {
Write-Host "DOWN: $svcName ($($svc.Status))" -ForegroundColor Red
if ($AttemptRestart) {
Write-Host " Attempting restart..."
try {
Restart-Service -Name $svcName -Force -ErrorAction Stop
Write-Host " Restarted successfully" -ForegroundColor Green
$stoppedServices += "$svcName - was $($svc.Status), RESTARTED"
} catch {
Write-Host " Restart failed: $_" -ForegroundColor Red
$stoppedServices += "$svcName - $($svc.Status), restart FAILED: $_"
}
} else {
$stoppedServices += "$svcName - $($svc.Status)"
}
} else {
Write-Host "OK: $svcName" -ForegroundColor Green
}
}
if ($stoppedServices.Count -gt 0) {
$body = @"
Service alert on $hostname
$($stoppedServices.Count) critical service(s) not running:
$($stoppedServices | ForEach-Object { " - $_" } | Out-String)
Full service status:
$($CriticalServices | ForEach-Object {
$s = Get-Service -Name $_ -ErrorAction SilentlyContinue
if ($s) { " $($_.PadRight(20)) $($s.Status)" } else { " $($_.PadRight(20)) NOT FOUND" }
} | Out-String)
Checked at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"@
nylas email send --to $AlertTo `
--subject "SERVICE ALERT: $hostname - $($stoppedServices.Count) service(s) down" `
--body $body --yes
}Windows Event Log alerts
The Windows Event Log captures errors, warnings, and critical events from the OS and applications. According to Microsoft, Event ID 6008 indicates an unexpected shutdown, and Event IDs 1000-1001 in the Application log signal application crashes. This script queries recent errors and emails a summary.
# eventlog-alert.ps1 -- Email recent Error and Critical events
param(
[int]$HoursBack = 1,
[string]$AlertTo = "ops@company.com"
)
$hostname = $env:COMPUTERNAME
$startTime = (Get-Date).AddHours(-$HoursBack)
# Query System and Application logs for Error and Critical events
$events = @()
foreach ($logName in @('System', 'Application')) {
$events += Get-WinEvent -FilterHashtable @{
LogName = $logName
Level = 1, 2 # 1=Critical, 2=Error
StartTime = $startTime
} -ErrorAction SilentlyContinue
}
if ($events.Count -eq 0) {
Write-Host "No errors in the last $HoursBack hour(s)"
exit 0
}
# Group by source for a summary
$grouped = $events | Group-Object ProviderName |
Sort-Object Count -Descending
$summary = $grouped | ForEach-Object {
"$($_.Count) event(s) from $($_.Name)"
}
# Get the 5 most recent events with details
$recent = $events | Sort-Object TimeCreated -Descending |
Select-Object -First 5 |
ForEach-Object {
@"
[$($_.TimeCreated.ToString('HH:mm:ss'))] $($_.ProviderName)
Event ID: $($_.Id) | Level: $($_.LevelDisplayName)
$($_.Message.Substring(0, [Math]::Min(200, $_.Message.Length)))
"@
}
$body = @"
Event Log alert on $hostname
$($events.Count) Error/Critical events in the last $HoursBack hour(s)
Summary by source:
$($summary | ForEach-Object { " $_" } | Out-String)
Recent events:
$($recent -join "`n`n")
Checked at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"@
nylas email send --to $AlertTo `
--subject "EVENT LOG: $hostname - $($events.Count) error(s) in last $($HoursBack)h" `
--body $body --yes
Write-Host "Alert sent: $($events.Count) events found"Scheduled task failure monitoring
Windows Task Scheduler tasks can fail silently. This script checks the last run result of critical tasks and alerts on failures.
# task-monitor.ps1 -- Alert on failed scheduled tasks
param(
[string[]]$TaskNames = @(
"NylasInboxReport",
"DatabaseBackup",
"LogCleanup"
),
[string]$AlertTo = "ops@company.com"
)
$hostname = $env:COMPUTERNAME
$failedTasks = @()
foreach ($taskName in $TaskNames) {
$task = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue
if (-not $task) {
$failedTasks += "$taskName - TASK NOT FOUND"
continue
}
# LastTaskResult 0 = success, anything else = failure
if ($task.LastTaskResult -ne 0) {
$hexResult = "0x{0:X}" -f $task.LastTaskResult
$lastRun = $task.LastRunTime.ToString('yyyy-MM-dd HH:mm:ss')
$failedTasks += "$taskName - Exit code $hexResult (last run: $lastRun)"
}
}
if ($failedTasks.Count -gt 0) {
$body = @"
Scheduled task alert on $hostname
$($failedTasks.Count) task(s) failed:
$($failedTasks | ForEach-Object { " - $_" } | Out-String)
Checked at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"@
nylas email send --to $AlertTo `
--subject "TASK ALERT: $hostname - $($failedTasks.Count) task(s) failed" `
--body $body --yes
} else {
Write-Host "All $($TaskNames.Count) tasks passed last run"
}CPU and memory threshold alerts
Sustained high CPU or memory usage for extended periods usually signals a runaway process or memory leak. This script checks both and includes the top processes by resource usage in the alert.
# resource-alert.ps1 -- Alert on high CPU or memory usage
param(
[int]$CpuThreshold = 90,
[int]$MemoryThreshold = 90,
[string]$AlertTo = "ops@company.com"
)
$hostname = $env:COMPUTERNAME
$alerts = @()
# CPU check (average over 5 samples, 2s apart)
$cpuAvg = (Get-Counter 'Processor(_Total)% Processor Time' `
-SampleInterval 2 -MaxSamples 5 |
ForEach-Object { $_.CounterSamples.CookedValue } |
Measure-Object -Average).Average
$cpuPct = [math]::Round($cpuAvg, 1)
if ($cpuPct -ge $CpuThreshold) {
$topCpu = Get-Process | Sort-Object CPU -Descending |
Select-Object -First 5 Name, @{N='CPU_Sec';E={[math]::Round($_.CPU, 1)}}, Id
$alerts += "CPU at $cpuPct% (threshold: $CpuThreshold%)"
$alerts += "Top processes by CPU:"
$topCpu | ForEach-Object { $alerts += " $($_.Name) (PID $($_.Id)): $($_.CPU_Sec)s" }
}
# Memory check
$os = Get-CimInstance Win32_OperatingSystem
$totalMB = [math]::Round($os.TotalVisibleMemorySize / 1024)
$freeMB = [math]::Round($os.FreePhysicalMemory / 1024)
$usedPct = [math]::Round((1 - $freeMB / $totalMB) * 100, 1)
if ($usedPct -ge $MemoryThreshold) {
$topMem = Get-Process | Sort-Object WorkingSet64 -Descending |
Select-Object -First 5 Name, @{N='MemMB';E={[math]::Round($_.WorkingSet64/1MB)}}, Id
$alerts += "Memory at $usedPct% ($freeMB MB free of $totalMB MB)"
$alerts += "Top processes by memory:"
$topMem | ForEach-Object { $alerts += " $($_.Name) (PID $($_.Id)): $($_.MemMB) MB" }
}
if ($alerts.Count -gt 0) {
$body = @"
Resource alert on $hostname
$($alerts -join "`n")
Checked at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"@
nylas email send --to $AlertTo `
--subject "RESOURCE ALERT: $hostname - CPU:$cpuPct% MEM:$usedPct%" `
--body $body --yes
} else {
Write-Host "Resources OK: CPU $cpuPct%, Memory $usedPct%"
}SSL certificate expiry alerts
According to the Netcraft Web Server Survey, 2.4% of HTTPS sites serve expired certificates at any given time. Catch yours before it expires.
# ssl-expiry-alert.ps1 -- Alert on certificates expiring within N days
param(
[string[]]$Domains = @("app.company.com", "api.company.com"),
[int]$WarningDays = 30,
[string]$AlertTo = "ops@company.com"
)
$expiring = @()
foreach ($domain in $Domains) {
try {
$req = [System.Net.HttpWebRequest]::Create("https://$domain")
$req.Timeout = 10000
$req.AllowAutoRedirect = $false
$resp = $req.GetResponse()
$cert = $req.ServicePoint.Certificate
$expiry = [DateTime]::Parse($cert.GetExpirationDateString())
$daysLeft = ($expiry - (Get-Date)).Days
$resp.Close()
if ($daysLeft -le $WarningDays) {
$expiring += "$domain expires $($expiry.ToString('yyyy-MM-dd')) ($daysLeft days)"
}
Write-Host "$domain - expires in $daysLeft days"
} catch {
$expiring += "$domain - FAILED TO CHECK: $_"
}
}
if ($expiring.Count -gt 0) {
$body = "SSL certificate expiry warning:`n`n$($expiring -join "`n")"
nylas email send --to $AlertTo `
--subject "SSL EXPIRY: $($expiring.Count) cert(s) within $WarningDays days" `
--body $body --yes
}Schedule all monitors with Task Scheduler
Register each monitoring script as a scheduled task. Run disk and service checks every 15 minutes, Event Log checks hourly, and SSL checks daily.
# Register monitoring scripts as scheduled tasks
$monitors = @(
@{ Name = "DiskAlert"; Script = "disk-alert.ps1"; Interval = 15 }
@{ Name = "ServiceMonitor"; Script = "service-monitor.ps1"; Interval = 15 }
@{ Name = "EventLogAlert"; Script = "eventlog-alert.ps1"; Interval = 60 }
@{ Name = "ResourceAlert"; Script = "resource-alert.ps1"; Interval = 5 }
@{ Name = "TaskMonitor"; Script = "task-monitor.ps1"; Interval = 60 }
@{ Name = "SSLExpiry"; Script = "ssl-expiry-alert.ps1"; Interval = 1440 }
)
foreach ($m in $monitors) {
$action = New-ScheduledTaskAction `
-Execute "pwsh.exe" `
-Argument "-NoProfile -File C:Monitoring$($m.Script)"
$trigger = New-ScheduledTaskTrigger `
-Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Minutes $m.Interval) `
-RepetitionDuration (New-TimeSpan -Days 365)
Register-ScheduledTask `
-TaskName "Nylas-$($m.Name)" `
-Action $action `
-Trigger $trigger `
-Description "$($m.Name) - runs every $($m.Interval) min"
Write-Host "Registered: Nylas-$($m.Name) (every $($m.Interval) min)"
}Frequently asked questions
How do I send disk space alerts from PowerShell?
Use Get-CimInstance Win32_LogicalDisk to check free space on each drive. Compare against a threshold (e.g., 10%). When a drive drops below, send an alert with nylas email send. Schedule the script with Task Scheduler to run every 15 minutes.
Can I monitor Windows services and get email alerts when they stop?
Yes. Use Get-Service to check status of critical services. When any service isn't running, the script can attempt a restart with Restart-Service and then email you the result. Add -AttemptRestart to enable auto-restart before alerting.
How do I email Windows Event Log errors from PowerShell?
Use Get-WinEvent -FilterHashtable to query Error and Critical events from the last hour. Group by source, format the top 5 events with details, and send via nylas email send. Schedule hourly to catch issues as they happen.
Next steps
- Automated email reports with PowerShell -- CSV attachments, HTML tables, weekly digests
- CI/CD email notifications -- build alerts, deployment reports from pipelines
- Send email from PowerShell -- the foundation guide for PowerShell email
- Full command reference -- every flag and subcommand