Build a repeatable PowerShell server health check covering uptime, disk space, services, memory and connectivity.
Define a fixed set of checks (uptime, disk, services, connectivity), run each, and return a consistent Pass/Fail object per check.
Run under a least-privilege service account — a health check script rarely needs local admin, and it's the last script you want compromised.
Choose the indicators that matter to the environment such as uptime, system drive free space, critical services and network reachability.
Define the checks and their thresholds
Decide on a fixed set of checks with explicit pass/fail thresholds before writing any code — a health check that just prints raw numbers requires a human to interpret every run, while one with thresholds can flag problems automatically. A reasonable baseline set: uptime since last reboot, system drive free space below a percentage, a defined list of critical services, and reachability of one or two dependent hosts (a DC, a database server).
Collect system data
$os = Get-CimInstance Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$disk = Get-Volume -DriveLetter C
$freePercent = [math]::Round(($disk.SizeRemaining / $disk.Size) * 100, 1)
Get-Volume is preferred over the older Get-CimInstance Win32_LogicalDisk for readability, but both work — use whichever your environment already standardizes on for consistency across scripts.
Check critical services
$criticalServices = 'DNS','Netlogon','W3SVC'
$serviceStatus = Get-Service -Name $criticalServices -ErrorAction SilentlyContinue |
Select-Object Name, Status
Include services that don't exist on this server in the results as a distinct "Not Found" status rather than silently skipping them — a service that should exist but doesn't (e.g., after a failed install) is itself a finding worth surfacing.
Check connectivity to dependent systems
$connectivity = Test-NetConnection -ComputerName "dc01.contoso.com" -Port 389 -InformationLevel Quiet
Use -InformationLevel Quiet to get a clean boolean back rather than the full verbose object — this is what makes the result easy to fold into a structured Pass/Fail report in the next step.
Return structured, consistent output
Every check should return the same shape of object — this is what makes the report scannable and easy to filter for failures only:
$results = @()
$results += [PSCustomObject]@{ Check='Uptime'; Status = if($uptime.TotalHours -lt 720){'OK'}else{'WARN'}; Detail = "$([math]::Round($uptime.TotalDays,1)) days" }
$results += [PSCustomObject]@{ Check='DiskFreeC'; Status = if($freePercent -gt 15){'OK'}else{'FAIL'}; Detail = "$freePercent% free" }
$results | Where-Object Status -ne 'OK'
Schedule with a least-privilege account
Run this via Task Scheduler with an account scoped only to what the checks actually need — most health checks don't require local admin, and a broadly-privileged scheduled task is an unnecessary risk if that account is ever compromised. Export failures only (not every run's full output) to keep the alert signal meaningful:
$today = Get-Date -Format "yyyy-MM-dd HH:mm"
$failures = $results | Where-Object Status -ne 'OK'
if ($failures) {
$failures | Export-Csv -Path "C:\Temp\HealthCheck_Failures_$today.csv" -NoTypeInformation
}
Full script
$os = Get-CimInstance Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$disk = Get-Volume -DriveLetter C
$freePercent = [math]::Round(($disk.SizeRemaining / $disk.Size) * 100, 1)
$criticalServices = 'DNS','Netlogon','W3SVC'
$serviceStatus = Get-Service -Name $criticalServices -ErrorAction SilentlyContinue
$results = @()
$results += [PSCustomObject]@{ Check='Uptime'; Status = if($uptime.TotalHours -lt 720){'OK'}else{'WARN'}; Detail = "$([math]::Round($uptime.TotalDays,1)) days" }
$results += [PSCustomObject]@{ Check='DiskFreeC'; Status = if($freePercent -gt 15){'OK'}else{'FAIL'}; Detail = "$freePercent% free" }
foreach ($svc in $criticalServices) {
$s = $serviceStatus | Where-Object Name -eq $svc
$status = if (-not $s) {'FAIL'} elseif ($s.Status -eq 'Running') {'OK'} else {'FAIL'}
$results += [PSCustomObject]@{ Check="Service:$svc"; Status = $status; Detail = if($s){$s.Status}else{'Not found'} }
}
$results