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.exeon 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.


