Introduction to Performance Monitoring on Windows Server 2019
Performance monitoring on Windows Server 2019 involves collecting, analyzing, and responding to metrics that describe the health and capacity of server resources including CPU, memory, disk, and network. Windows Server 2019 includes several built-in tools for performance monitoring: Performance Monitor (perfmon.exe) for real-time and historical counter collection, Resource Monitor for interactive process and resource analysis, Task Manager for quick system overview, and Windows Performance Recorder and Analyzer for deep profiling. PowerShell provides scripting access to performance data through WMI/CIM, and integration with Azure Monitor enables cloud-hosted performance analytics. Proactive performance monitoring helps prevent incidents, plan capacity, and identify bottlenecks before they affect users.
Using Performance Monitor
Performance Monitor (perfmon.exe) is the primary Windows tool for collecting performance counter data. Open it from the Start menu or run:
perfmon.exe
# Or open directly to the monitoring view
perfmon /sys
Add performance counters by clicking the green plus button in the real-time view. Essential counters to monitor on Windows Server 2019:
CPU counters: Processor(_Total)% Processor Time (sustained above 80% indicates bottleneck), Processor(_Total)% Privileged Time (high kernel time may indicate driver issues), SystemProcessor Queue Length (above 2 per CPU indicates CPU saturation), SystemContext Switches/sec.
Memory counters: MemoryAvailable Bytes (less than 300 MB is critical), MemoryPages/sec (consistently above 20 indicates memory pressure), MemoryPage Faults/sec, Memory% Committed Bytes In Use.
Disk counters: PhysicalDisk(_Total)Avg. Disk sec/Read (above 20ms indicates disk latency), PhysicalDisk(_Total)Avg. Disk sec/Write, PhysicalDisk(_Total)Disk Queue Length (above 2 per spindle), PhysicalDisk(_Total)Disk Bytes/sec.
Network counters: Network Interface(*)Bytes Total/sec, Network Interface(*)Current Bandwidth, Network Interface(*)Output Queue Length.
Creating Data Collector Sets
Data Collector Sets automate the collection of performance data over time. Create one from Performance Monitor’s Data Collector Sets section or via command line:
logman create counter "Server Baseline" -f csv -o "C:PerfDataServerBaseline" -si 60 -c "Processor(_Total)% Processor Time" "MemoryAvailable Bytes" "MemoryPages/sec" "PhysicalDisk(_Total)Avg. Disk sec/Read" "PhysicalDisk(_Total)Avg. Disk sec/Write" "Network Interface(*)Bytes Total/sec" "SystemProcessor Queue Length"
logman start "Server Baseline"
logman stop "Server Baseline"
logman query "Server Baseline"
Schedule a Data Collector Set to run automatically at a specific time:
logman create counter "Weekly Baseline" -f csv -o "C:PerfDataWeeklyBaseline" -si 30 -b 06/01/2026:09:00:00 -e 06/01/2026:17:00:00 -c "Processor(_Total)% Processor Time" "MemoryAvailable MBytes" "PhysicalDisk(*)Disk Reads/sec" "PhysicalDisk(*)Disk Writes/sec"
Using Get-Counter in PowerShell
PowerShell’s Get-Counter cmdlet provides scripted access to performance counter data, enabling automated monitoring and reporting. Retrieve a snapshot of key counters:
Get-Counter 'Processor(_Total)% Processor Time','MemoryAvailable Bytes','MemoryPages/sec' -SampleInterval 1 -MaxSamples 5 | ForEach-Object {
$_.CounterSamples | Select-Object Path, CookedValue
} | Format-Table -AutoSize
Build a continuous monitoring loop with thresholds and alerting:
while ($true) {
$cpu = (Get-Counter 'Processor(_Total)% Processor Time').CounterSamples.CookedValue
$memMB = (Get-Counter 'MemoryAvailable MBytes').CounterSamples.CookedValue
$diskQueue = (Get-Counter 'PhysicalDisk(_Total)Disk Queue Length').CounterSamples.CookedValue
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "$timestamp | CPU: $([math]::Round($cpu,1))% | MemAvail: ${memMB}MB | DiskQueue: $([math]::Round($diskQueue,2))"
Add-Content -Path "C:MonitoringPerfLog.txt" -Value $logLine
if ($cpu -gt 90) { Write-Warning "HIGH CPU: $cpu%" }
if ($memMB -lt 512) { Write-Warning "LOW MEMORY: ${memMB}MB available" }
Start-Sleep -Seconds 60
}
Analyzing Performance with Resource Monitor
Resource Monitor provides a live, interactive view of CPU, memory, disk, and network resource usage per process. Launch it:
resmon.exe
# Or from Task Manager > Performance tab > Open Resource Monitor
Resource Monitor is especially useful for identifying which process is consuming excessive disk I/O or holding file locks. The Disk tab shows all disk reads and writes per process. The Network tab shows active TCP connections with process associations. The Memory tab shows working set, shareable, and private memory per process.
Using WMI for Performance Data
Windows Management Instrumentation (WMI) provides programmatic access to performance data through CIM classes. Query performance data using WMI:
Get-CimInstance Win32_Processor | Select-Object Name, LoadPercentage, NumberOfCores, MaxClockSpeed
Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory, TotalVirtualMemorySize, FreeVirtualMemory | ForEach-Object {
[PSCustomObject]@{
TotalMemoryGB = [math]::Round($_.TotalVisibleMemorySize/1MB, 2)
FreeMemoryGB = [math]::Round($_.FreePhysicalMemory/1MB, 2)
UsedMemoryGB = [math]::Round(($_.TotalVisibleMemorySize - $_.FreePhysicalMemory)/1MB, 2)
MemoryUtilPct = [math]::Round((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory)/$_.TotalVisibleMemorySize)*100, 1)
}
}
Get-CimInstance Win32_LogicalDisk | Select-Object DeviceID, Size, FreeSpace | ForEach-Object {
[PSCustomObject]@{
Drive = $_.DeviceID
TotalGB = [math]::Round($_.Size/1GB, 2)
FreeGB = [math]::Round($_.FreeSpace/1GB, 2)
FreePercent = [math]::Round(($_.FreeSpace/$_.Size)*100, 1)
}
}
Configuring Performance Alerts with Windows
Configure Windows to generate alerts when performance thresholds are exceeded using Performance Monitor’s Data Collector Sets with Alert type, or using Task Scheduler with PowerShell scripts. Create a performance alert for high CPU:
logman create alert "HighCPUAlert" -th "Processor(_Total)% Processor Time>85" -si 30 -task "C:ScriptsAlert-HighCPU.ps1"
Create the alert script that sends email notification:
$smtpServer = "mail.contoso.com"
$from = "[email protected]"
$to = "[email protected]"
$subject = "HIGH CPU Alert on $env:COMPUTERNAME"
$body = "CPU utilization exceeded 85% at $(Get-Date). Current: $((Get-Counter 'Processor(_Total)% Processor Time').CounterSamples.CookedValue)%"
Send-MailMessage -SmtpServer $smtpServer -From $from -To $to -Subject $subject -Body $body
Using Windows Performance Toolkit
Windows Performance Recorder (WPR) and Windows Performance Analyzer (WPA) are part of the Windows Assessment and Deployment Kit and provide deep system profiling for troubleshooting performance issues. Capture a performance trace:
wpr -start GeneralProfile -start CPU -start DiskIO -start Network
# Reproduce the performance issue...
wpr -stop C:PerfTracesCapture_$(Get-Date -Format yyyyMMdd_HHmmss).etl
wpa C:PerfTracesCapture_*.etl
Establishing Performance Baselines
A performance baseline documents normal resource utilization during typical workloads. Establish baselines by collecting data over one to two weeks of normal operation. Use the Baseline Data Collector Set and analyze the results. Create a baseline report script:
$samples = 12 # 12 samples x 5 min interval = 1 hour
$counters = @('Processor(_Total)% Processor Time','MemoryAvailable MBytes','PhysicalDisk(_Total)Avg. Disk sec/Transfer','Network Interface(*)Bytes Total/sec')
$data = Get-Counter -Counter $counters -SampleInterval 300 -MaxSamples $samples
$report = $data.CounterSamples | Group-Object Path | ForEach-Object {
[PSCustomObject]@{
Counter = $_.Name
Average = [math]::Round(($_.Group.CookedValue | Measure-Object -Average).Average, 2)
Maximum = [math]::Round(($_.Group.CookedValue | Measure-Object -Maximum).Maximum, 2)
Minimum = [math]::Round(($_.Group.CookedValue | Measure-Object -Minimum).Minimum, 2)
}
}
$report | Format-Table -AutoSize
$report | Export-Csv "C:BaselinesBaselineReport_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation