PowerShell Remote Desktop Services Monitoring Script
─□✕

PowerShell Remote Desktop Services Monitoring Script

PowerShell Tips Editor 5 min read
PowerShell Remote Desktop Services Monitoring Script

Why RDS Session Management Is a Recurring Admin Task

Remote Desktop Services sessions accumulate quietly on server farms. Users disconnect without logging off, licensing servers count those sessions against the CAL pool, and orphaned processes keep consuming memory on the RDS host. The Server Manager GUI shows you one server at a time and offers no scheduling or alerting. A PowerShell script that inventories sessions across your entire RDS farm, detects disconnected sessions, and fires threshold alerts costs a few hours to build but saves a recurring weekly manual task indefinitely.

Quick Answer

Parse qwinsta.exe output or query Win32_LogonSession via CIM to enumerate sessions. Filter for Disc state to find disconnected sessions, then wrap logoff.exe in PowerShell to clean them up. Use Invoke-Command to run all of this across multiple RDS hosts simultaneously.

Enumerating Sessions with qwinsta Output Parsing

qwinsta.exe is the fastest way to list sessions on a remote host. Its output is a fixed-width text table, so parsing it requires splitting on whitespace with careful column mapping. The first column is the session name, the second is the username, the third is the session ID, and the fourth is the state. Wrapping this in a function makes it reusable across the farm.

function Get-RDSSession {
    param([string]$ComputerName = $env:COMPUTERNAME)

    $raw = qwinsta /server:$ComputerName 2>&1
    $sessions = foreach ($line in $raw | Select-Object -Skip 1) {
        if ($line -match '^\s*(\S+)\s+(\S+)\s+(\d+)\s+(\S+)') {
            [PSCustomObject]@{
                SessionName  = $Matches[1]
                UserName     = $Matches[2]
                SessionId    = [int]$Matches[3]
                State        = $Matches[4]
                ComputerName = $ComputerName
            }
        }
    }
    $sessions | Where-Object { $_.UserName -notmatch '^[0-9]+$' }
}

Get-RDSSession -ComputerName "RDS01" | Format-Table -AutoSize

Using CIM Win32_LogonSession for Structured Data

For structured data without regex parsing, query Win32_LogonSession via CIM. LogonType 10 corresponds to RemoteInteractive (RDP) sessions. Joining with Win32_LoggedOnUser reveals the associated account. CIM returns proper typed objects that integrate cleanly into pipelines without the fragile string parsing that qwinsta requires.

$session = New-CimSession -ComputerName "RDS01" -ErrorAction Stop

$rdpSessions = Get-CimInstance -CimSession $session -ClassName Win32_LogonSession |
    Where-Object { $_.LogonType -eq 10 }

foreach ($logon in $rdpSessions) {
    $user = Get-CimAssociatedInstance -CimSession $session `
        -InputObject $logon -ResultClassName Win32_Account
    [PSCustomObject]@{
        LogonId  = $logon.LogonId
        UserName = $user.Name
        Domain   = $user.Domain
        LogonTime = $logon.StartTime
    }
}

Remove-CimSession $session

Detecting Disconnected vs Active Sessions

The State field from qwinsta reports Active for live sessions and Disc for disconnected ones. Sessions with state Disc are orphans — the user closed their RDP client without logging off. Filtering and reporting these separately is the core of useful RDS monitoring. You can also calculate how long a session has been disconnected using the session’s idle time if you include the /v verbose flag.

$servers = @("RDS01","RDS02","RDS03")

$allSessions = foreach ($srv in $servers) {
    Get-RDSSession -ComputerName $srv
}

$disconnected = $allSessions | Where-Object { $_.State -eq "Disc" }
$active       = $allSessions | Where-Object { $_.State -eq "Active" }

Write-Host "Active sessions  : $($active.Count)"
Write-Host "Disconnected     : $($disconnected.Count)"

$disconnected | Format-Table ComputerName, UserName, SessionId, State -AutoSize

Remote Session Logoff with logoff.exe Wrapped in PowerShell

Once disconnected sessions are identified, logoff.exe terminates them by session ID on the target server. Wrapping it in PowerShell allows you to add confirmation prompts, dry-run modes, and logging. Always exclude session ID 0 (the console session) and session ID 65536 (Services session) — logging those off will crash the server.

$protectedSessions = @(0, 65536)

foreach ($disc in $disconnected) {
    if ($disc.SessionId -in $protectedSessions) {
        Write-Warning "Skipping protected session $($disc.SessionId) on $($disc.ComputerName)"
        continue
    }

    Write-Host "Logging off session $($disc.SessionId) for $($disc.UserName) on $($disc.ComputerName)"
    logoff $disc.SessionId /server:$disc.ComputerName
}

Write-Host "Logoff sweep complete."

Monitoring Session Count for License Threshold Alerts

RDS CAL licensing violations occur quietly. Tracking total session count across the farm and alerting when it approaches the licensed limit gives operations teams enough notice to add capacity or audit unnecessary sessions. Send alerts via email or write to a Windows event log for integration with monitoring platforms like Zabbix or SCOM.

$licensedSessions = 50  # Update to your actual CAL count
$warnAt = [int]($licensedSessions * 0.85)

$totalActive = ($allSessions | Where-Object { $_.State -eq "Active" }).Count

if ($totalActive -ge $warnAt) {
    $msg = "RDS session alert: $totalActive of $licensedSessions sessions in use"
    Write-EventLog -LogName Application -Source "RDSMonitor" `
        -EventId 1001 -EntryType Warning -Message $msg
    Write-Warning $msg
}

Scheduled Daily Session Report

Wrapping the full session inventory in a scheduled script produces a daily CSV that tracks RDS utilization over time. Register it as a scheduled task on a management server and configure it to email the output to the operations team. Keeping historical session data helps justify capacity planning decisions.

$reportPath = "C:\Reports\RDS_Sessions_$(Get-Date -Format yyyyMMdd_HHmm).csv"

$allSessions |
    Select-Object ComputerName, UserName, SessionId, State |
    Export-Csv -Path $reportPath -NoTypeInformation

Write-Host "Daily RDS session report saved to $reportPath"

Common Errors

  • qwinsta output parsing breaks between OS versions. The column widths differ between Windows Server 2016 and 2022. Use a regex that anchors on the numeric session ID in column three rather than fixed character positions. Testing on each OS version in your farm is essential before deploying to production.
  • Logging off session 0 crashes the server. Session ID 0 is the console/services session. Passing it to logoff.exe on a server OS terminates critical system processes. Always include a guard list of protected session IDs and validate before every logoff operation.

Related Cmdlets / See Also

Wrapping Up

Automated RDS session monitoring replaces tedious manual checks with a scheduled script that inventories sessions, flags orphans, and fires license threshold alerts. Combining qwinsta parsing for quick enumeration with CIM queries for structured data gives you both speed and reliability. Schedule the full report daily to maintain a utilization history that supports licensing and capacity decisions.

Send-Item -To