How to Monitor IIS Performance with Counters on Windows Server 2025
IIS on Windows Server 2025 exposes a rich set of performance counters through the Windows Performance Monitor (perfmon) subsystem, covering everything from raw TCP connection counts to ASP.NET request queue depth and .NET garbage collection pressure. Proactive monitoring of these counters is essential for identifying bottlenecks before users experience slow page loads or 503 errors. This tutorial walks through the most important IIS-specific counters, how to collect them with PowerShell’s Get-Counter cmdlet, how to build a reusable Data Collector Set, how to enable Failed Request Tracing for slow-request diagnosis, and finally how to expose these metrics to a Grafana dashboard via Prometheus and the windows_exporter agent.
Prerequisites
- Windows Server 2025 with IIS 10 installed and at least one active site
- PowerShell 5.1 or later running as Administrator
- Performance Monitor (perfmon.exe) — included with Windows Server
- Optional: Prometheus windows_exporter for metrics export to Grafana
- IIS Failed Request Tracing feature installed (optional but recommended)
Step 1: Key IIS Performance Counters to Monitor
The following counter categories are the most relevant for IIS health monitoring:
Web Service Counters
Web ServiceCurrent Connections— active TCP connections per site; a sustained high value may indicate slow application responses holding connections openWeb ServiceTotal Requests/sec— overall request throughput; baseline this during normal load to detect anomaliesWeb ServiceBytes Sent/secandBytes Received/sec— network I/O; useful for detecting large response payloads saturating bandwidthWeb ServiceNot Found Errors/sec— 404 rate; a spike often indicates broken links, a scraping bot, or a missing deployment artifactWeb ServiceConnection Attempts/sec— raw connection rate before IIS processes them
ASP.NET Counters
ASP.NETRequests Queued— requests waiting for a thread; non-zero values under normal load indicate thread pool exhaustionASP.NETRequests Rejected— requests dropped because the queue is full; a direct indicator of overloadASP.NET ApplicationsRequests/Sec— application-level throughput (as opposed to IIS-level)ASP.NET ApplicationsRequest Execution Time— average execution time in milliseconds; should be under 200 ms for typical web applications
.NET CLR Counters
.NET CLR Memory# Gen 2 Collections— frequent Gen 2 GC pauses cause latency spikes; investigate large object heap allocations if this is elevated.NET CLR Memory% Time in GC— percentage of CPU time spent in garbage collection; over 10% indicates memory pressure.NET CLR Exceptions# of Exceps Thrown / sec— a high exception rate adds overhead even if exceptions are caught
Step 2: Querying Counters with PowerShell
The Get-Counter cmdlet samples counters on demand or at a defined interval without opening perfmon. This is useful for scripted health checks and logging.
# Retrieve a single sample of key IIS counters
$counters = @(
"Web Service(_Total)Current Connections",
"Web Service(_Total)Total Requests/sec",
"Web Service(_Total)Bytes Sent/sec",
"ASP.NETRequests Queued",
"ASP.NETRequests Rejected",
".NET CLR Memory(_Global_)% Time in GC"
)
Get-Counter -Counter $counters -SampleInterval 5 -MaxSamples 12 |
ForEach-Object { $_.CounterSamples } |
Select-Object Timestamp, Path, CookedValue |
Format-Table -AutoSize
To log continuously to a CSV file for later analysis:
Get-Counter -Counter $counters -SampleInterval 10 -Continuous |
ForEach-Object {
$_.CounterSamples | ForEach-Object {
[PSCustomObject]@{
Time = $_.Timestamp
Path = $_.Path
Value = [math]::Round($_.CookedValue, 2)
}
}
} |
Export-Csv -Path "C:Logsiis_perf_$(Get-Date -Format yyyyMMdd_HHmm).csv" `
-Append -NoTypeInformation
Step 3: Creating a Data Collector Set for IIS
A Data Collector Set (DCS) runs in the background and saves counter data to a binary log file (BLG) that can be opened in perfmon for historical analysis.
# Create DCS via logman (command-line tool for managing data collectors)
logman create counter "IIS-Monitor" `
--v `
-si 30 `
-f bincirc `
-max 512 `
-o "C:PerfLogsIIS-Monitor" `
-c "Web Service(_Total)*" `
"ASP.NET*" `
"ASP.NET Applications(__Total__)*" `
".NET CLR Memory(w3wp)*"
# Start the collector
logman start "IIS-Monitor"
# View its status
logman query "IIS-Monitor"
# Stop after your capture window
logman stop "IIS-Monitor"
Open the resulting .blg file in perfmon (perfmon /sys) via Performance Monitor > Open Log File to replay and graph the captured data.
Step 4: Enabling Failed Request Tracing for Slow Requests
Failed Request Tracing (FRT) captures detailed execution traces for requests that exceed a time threshold or return specific status codes. It is invaluable for diagnosing why individual requests are slow.
# Install the FRT feature if not already present
Install-WindowsFeature Web-Http-Tracing
# Enable FRT on a specific site (replace "Default Web Site" with your site name)
$siteName = "Default Web Site"
$logPath = "C:inetpublogsFailedReqLogFiles"
Set-WebConfigurationProperty `
-PSPath "IIS:Sites$siteName" `
-Filter "system.webServer/tracing/traceFailedRequests" `
-Name enabled -Value $true
# Trace all requests taking longer than 5 seconds (5000 ms)
Add-WebConfigurationProperty `
-PSPath "IIS:Sites$siteName" `
-Filter "system.webServer/tracing/traceFailedRequests" `
-Name "." `
-Value @{
path = "*"
timeTaken = "00:00:05"
statusCodes = "200-999"
}
FRT log files are written as XML to %SystemDrive%inetpublogsFailedReqLogFiles and can be opened in Internet Explorer or any XML viewer for a call-stack-style trace of every IIS and ASP.NET processing stage.
Step 5: Monitoring Application Pool Worker Process Memory
Each IIS Application Pool runs in a w3wp.exe worker process. Monitoring private bytes per pool helps detect memory leaks before they cause pool recycling under load.
# List all running w3wp.exe processes with their application pool names
Get-Process w3wp | ForEach-Object {
$pid = $_.Id
$poolName = (Get-Item "IIS:AppPools*" | Where-Object {
(Get-WebConfiguration "system.applicationHost/applicationPools/add[@name='$($_.Name)']/workerProcesses/add[@processId='$pid']")
}).Name
# Fallback: use WMI to find the pool name from the process command line
$cmdLine = (Get-WmiObject Win32_Process -Filter "ProcessId = $pid").CommandLine
$poolFromCmd = if ($cmdLine -match "-aps+""([^""]+)""") { $matches[1] } else { "Unknown" }
[PSCustomObject]@{
PID = $pid
AppPool = $poolFromCmd
PrivateMB = [math]::Round($_.WorkingSet64 / 1MB, 1)
VirtualMB = [math]::Round($_.VirtualMemorySize64 / 1MB, 1)
CPU_sec = [math]::Round($_.TotalProcessorTime.TotalSeconds, 1)
Handles = $_.HandleCount
}
} | Format-Table -AutoSize
Step 6: Exporting IIS Metrics to Grafana via Prometheus
windows_exporter is a Prometheus metrics agent for Windows that exposes perfmon counters in Prometheus format over HTTP. Install it to feed IIS data into a Grafana dashboard.
# Download the latest windows_exporter MSI
Invoke-WebRequest `
-Uri "https://github.com/prometheus-community/windows_exporter/releases/latest/download/windows_exporter-amd64.msi" `
-OutFile "$env:TEMPwindows_exporter.msi"
# Install with the IIS collector enabled
Start-Process msiexec.exe `
-ArgumentList '/i "$env:TEMPwindows_exporter.msi" ENABLED_COLLECTORS=iis,process,memory,cpu /quiet' `
-Wait
# The exporter runs as a Windows service on port 9182 by default
# Verify: http://localhost:9182/metrics
Add the Windows target to your Prometheus prometheus.yml:
scrape_configs:
- job_name: 'windows_iis'
static_configs:
- targets: ['10.10.1.11:9182']
relabel_configs:
- source_labels: [__address__]
target_label: instance
In Grafana, import dashboard ID 14694 (Windows Exporter Full) or 14510 (IIS-focused) from grafana.com to get pre-built panels for IIS connections, request rates, and ASP.NET queue depth.
Conclusion
A comprehensive IIS monitoring strategy on Windows Server 2025 combines real-time counter sampling with Get-Counter, persistent historical logging via Data Collector Sets, detailed slow-request traces through Failed Request Tracing, and optionally a time-series pipeline to Grafana via windows_exporter. Establishing baselines for key metrics like Requests/sec, Requests Queued, and % Time in GC during normal operation makes it far easier to recognize anomalies when they occur and correlate IIS-level symptoms — such as a rising queue depth — with application-level causes like database query regressions or memory leaks in a specific application pool.