How to Monitor IIS Performance with Counters on Windows Server 2022

IIS on Windows Server 2022 exposes a rich set of performance counters through the Windows Performance Counter infrastructure. Monitoring these counters is essential for diagnosing slow response times, high CPU usage, request queue buildup, and memory pressure in web applications. This guide covers the key IIS performance counters, how to use Performance Monitor, command-line tools, and PowerShell to collect and alert on them.

Key IIS Performance Counter Categories

IIS performance counters are organized into several counter objects. The most important are: Web Service (per-site and aggregate IIS metrics), W3SVC_W3WP (worker process metrics per application pool), ASP.NET Applications (for classic ASP.NET apps), ASP.NET (global ASP.NET runtime counters), and .NET CLR Memory / .NET CLR Exceptions (for managed application diagnostics).

For native IIS, the Web Service and W3SVC_W3WP categories are the primary targets. For .NET applications running on IIS, supplement these with ASP.NET Applications counters.

Critical Web Service Counters

The Web Service counter category reports metrics for each IIS website individually and for the _Total aggregate across all sites. The most important counters are:

Web Service(_Total)Current Connections
Web Service(_Total)Connection Attempts/sec
Web Service(_Total)Total Method Requests/sec
Web Service(_Total)Get Requests/sec
Web Service(_Total)Post Requests/sec
Web Service(_Total)Bytes Sent/sec
Web Service(_Total)Bytes Received/sec
Web Service(_Total)Total Files Sent
Web Service(_Total)Not Found Errors/sec
Web Service(_Total)Locked Errors/sec

Current Connections is the number of active connections to the web service at the moment of sampling. Not Found Errors/sec tracks 404 responses—a spike indicates a broken deployment or a crawler hitting non-existent URLs. Locked Errors/sec tracks 423 Locked responses, which typically indicates WebDAV locking issues.

W3SVC_W3WP Worker Process Counters

The W3SVC_W3WP category exposes per-application-pool worker process metrics. Each instance name corresponds to an application pool name. These counters are critical for identifying per-app resource consumption:

W3SVC_W3WP(*)Requests/Sec
W3SVC_W3WP(*)Active Requests
W3SVC_W3WP(*)Request Wait Time
W3SVC_W3WP(*)Cache: URI Cache Hits/sec
W3SVC_W3WP(*)Cache: URI Cache Misses/sec
W3SVC_W3WP(*)Cache: Output Cache Current Entries
W3SVC_W3WP(*)% 401 HTTP Response Sent
W3SVC_W3WP(*)% 500 HTTP Response Sent

Active Requests is the number of requests being processed right now. If this number grows continuously under load, your application is not processing requests fast enough. Request Wait Time is the elapsed time in milliseconds for the most recently completed request—a sustained value over 1000 ms warrants investigation.

Adding Counters in Performance Monitor

Open Performance Monitor by running perfmon.exe or searching in Server Manager → Tools → Performance Monitor. In the Monitoring Tools → Performance Monitor view, click the green plus (+) button to add counters.

# Open Performance Monitor directly
perfmon.exe

# Or open via run dialog
# Win + R → perfmon

In the Add Counters dialog, select the computer (local or remote), expand the counter category (e.g., “Web Service”), select the specific counters and instances you want, and click Add. For IIS monitoring, add counters from at minimum: Web Service, W3SVC_W3WP, Process (for w3wp.exe memory), and Processor (for CPU by process).

To monitor memory per application pool, use the Process counter category, filter by instances named w3wp or w3wp#1, w3wp#2 etc., and add Private Bytes and Working Set - Private. Correlate the PID of each w3wp.exe to its application pool using the command below:

# Identify which w3wp PID belongs to which app pool
%windir%system32inetsrvappcmd.exe list wp

# Output example:
# WP "12345" (applicationPool:DefaultAppPool)
# WP "67890" (applicationPool:MyApp)

typeperf for Command-Line Monitoring

typeperf.exe is a built-in command-line tool that writes performance counter samples to stdout or a CSV file. It is useful for capturing a time-series of IIS counter values during a load test or performance investigation without the overhead of the GUI.

# Sample IIS current connections every 5 seconds, 60 samples
typeperf "Web Service(_Total)Current Connections" -si 5 -sc 60

# Sample multiple counters to a CSV file
typeperf `
  "Web Service(_Total)Current Connections" `
  "Web Service(_Total)Get Requests/sec" `
  "W3SVC_W3WP(_Total)Active Requests" `
  "W3SVC_W3WP(_Total)Request Wait Time" `
  -si 5 -sc 120 -o C:Logsiis_perf.csv -f CSV

# List all available counters in the Web Service category
typeperf -q "Web Service"

# List all available counters in all categories
typeperf -qx

The -si flag sets the sample interval in seconds, -sc sets the sample count, -o sets the output file path, and -f sets the format (CSV, TSV, BIN). The resulting CSV can be opened in Excel or imported into monitoring tools for analysis.

Get-Counter in PowerShell

The Get-Counter cmdlet in PowerShell provides a native way to query performance counters and export results without external tools. It returns structured objects that you can pipe to further processing.

# Get a single sample of current connections
Get-Counter "Web Service(_Total)Current Connections"

# Continuous sampling every 2 seconds
Get-Counter "Web Service(_Total)Current Connections" -SampleInterval 2 -Continuous

# Collect multiple counters with 30 samples at 5-second intervals
$counters = @(
    "Web Service(_Total)Current Connections",
    "Web Service(_Total)Get Requests/sec",
    "W3SVC_W3WP(_Total)Active Requests",
    "W3SVC_W3WP(_Total)Request Wait Time"
)

$results = Get-Counter -Counter $counters -SampleInterval 5 -MaxSamples 30

# Export to CSV
$results.CounterSamples | Select-Object Path, CookedValue, Timestamp |
    Export-Csv -Path C:Logsiis_counters.csv -NoTypeInformation

# List all W3SVC_W3WP instances (app pools)
(Get-Counter -ListSet W3SVC_W3WP).PathsWithInstances |
    Select-String "Active Requests"

Monitoring w3wp.exe Memory and CPU per App Pool

Each IIS application pool runs as one or more w3wp.exe worker processes. Monitoring per-process CPU and memory helps identify which application pool is consuming resources. Use a combination of Get-Process and appcmd.exe to correlate PIDs to app pools:

# Get all w3wp.exe processes with CPU and memory
Get-Process w3wp | Select-Object Id, CPU, WorkingSet, PrivateMemorySize, `
    @{Name="WorkingSetMB"; Expression={[Math]::Round($_.WorkingSet/1MB,2)}}, `
    @{Name="PrivateMB"; Expression={[Math]::Round($_.PrivateMemorySize/1MB,2)}}

# Map PIDs to app pool names
$appPools = & "$env:windirsystem32inetsrvappcmd.exe" list wp
foreach ($line in $appPools) {
    if ($line -match 'WP "(d+)" (applicationPool:(.*?))') {
        $pid_num = $Matches[1]
        $pool    = $Matches[2]
        $proc    = Get-Process -Id $pid_num -ErrorAction SilentlyContinue
        [PSCustomObject]@{
            AppPool    = $pool
            PID        = $pid_num
            CPU        = $proc.CPU
            WorkingSetMB = [Math]::Round($proc.WorkingSet/1MB, 2)
        }
    }
} | Format-Table -AutoSize

Request Queue Length

The HTTP.sys kernel-mode driver maintains a request queue before requests are dispatched to w3wp.exe. When the worker process cannot accept requests fast enough, requests pile up in this queue. Monitor the queue length with the HTTP Service Request Queues counter category:

# Monitor HTTP.sys request queue length per app pool
Get-Counter "HTTP Service Request Queues(*)CurrentQueueSize" -SampleInterval 2 -MaxSamples 30

# Also useful: arrival rate vs active count
Get-Counter @(
    "HTTP Service Request Queues(*)CurrentQueueSize",
    "HTTP Service Request Queues(*)ArrivalRate"
) -SampleInterval 5 -MaxSamples 20

A sustained queue length above zero means your application pool is a bottleneck. Increasing the number of worker processes (web garden) or scaling the application logic are the typical remedies. The default maximum queue length in HTTP.sys is 1000 requests per app pool.

Failed Requests Per Second

IIS Failed Request Tracing (FREB) is the detailed diagnostic tool, but for trending purposes, use performance counters to track error rates:

# Monitor HTTP error responses by type
$errorCounters = @(
    "W3SVC_W3WP(_Total)% 401 HTTP Response Sent",
    "W3SVC_W3WP(_Total)% 403 HTTP Response Sent",
    "W3SVC_W3WP(_Total)% 404 HTTP Response Sent",
    "W3SVC_W3WP(_Total)% 500 HTTP Response Sent"
)
Get-Counter -Counter $errorCounters -SampleInterval 10 -MaxSamples 6

Creating Alerts on Thresholds

Performance Monitor can trigger alerts when a counter crosses a threshold. In Performance Monitor, expand Data Collector Sets → User Defined, right-click and create a new Data Collector Set. Choose “Performance Counter Alert” and add counters with their thresholds and alert actions.

For scripted alerting, use a PowerShell loop with Get-Counter and send an email or write to the event log when thresholds are exceeded:

# Alert when active requests exceed 50
while ($true) {
    $sample = Get-Counter "W3SVC_W3WP(_Total)Active Requests"
    $value  = $sample.CounterSamples[0].CookedValue
    if ($value -gt 50) {
        Write-EventLog -LogName Application -Source "IIS Monitor" `
            -EventId 9001 -EntryType Warning `
            -Message "IIS Active Requests exceeded threshold: $value"
    }
    Start-Sleep -Seconds 30
}

# Register the event source first (one time)
New-EventLog -LogName Application -Source "IIS Monitor"

Prometheus IIS Exporter Overview

For modern observability stacks using Prometheus and Grafana, the windows_exporter (formerly wmi_exporter) can expose IIS performance counters as Prometheus metrics over HTTP. Install it as a Windows service and enable the IIS collector.

# Download windows_exporter (check latest release on GitHub)
# https://github.com/prometheus-community/windows_exporter/releases

# Install as a service with IIS collector enabled
windows_exporter-0.28.0-amd64.exe `
    --collectors.enabled "cpu,cs,iis,logical_disk,memory,net,os,process,service,system" `
    --service.name "windows_exporter"

# Verify metrics are exposed (default port 9182)
Invoke-WebRequest -Uri "http://localhost:9182/metrics" -UseBasicParsing |
    Select-Object -ExpandProperty Content | Select-String "windows_iis"

The windows_exporter exposes IIS metrics as windows_iis_* Prometheus metrics. Common metrics include windows_iis_current_connections, windows_iis_requests_total, and windows_iis_worker_requests_total per application pool. These can be graphed in Grafana using the official IIS dashboard from the Grafana marketplace or a custom dashboard.