How to find and log off RDP sessions in Windows with quser and qwinsta

Use quser, qwinsta and rwinsta to list RDP sessions on a Windows server, spot disconnected ones blocking new logons, and log them off from the command line.

quser, qwinsta and rwinsta answer the question every server admin asks under pressure: who is on this machine, and how do I get them off it. They are the short names for query user, query session and reset session, and they ship with every Pro and Server edition of Windows. No RSAT, no modules, no GUI.

This matters more than it looks. A Windows Server allows two administrative RDP sessions; a Remote Desktop Services host has a licence-bound limit. Sessions that users close with the X button do not end, they go to Disc – and a disconnected session keeps its RAM, its file handles and its loaded profile. The third person trying to connect gets a refusal that has nothing to do with the network.

Microsoft documents each of these commands on its own page. What is missing there is the workflow that joins them, the difference in what quser and qwinsta actually show you, and the one output quirk that silently breaks almost every script written around quser.

Applies to: Windows 10 / 11 Pro and Enterprise, Windows Server 2016 / 2019 / 2022


Quick answer

List the sessions, warn the user, then release the session. Three commands, run from an elevated prompt on your own workstation – you do not need to be logged on to the target server.

rem 1. who is on the box, and what is each session ID
quser /server:SRV-PROD-01

rem 2. warn before you disrupt anyone - message stays on screen for 30 seconds
msg 3 /server:SRV-PROD-01 /time:30 "Your disconnected session will be closed in 1 minute."

rem 3. end session ID 3 gracefully - profile is saved, processes are closed properly
logoff 3 /server:SRV-PROD-01
Warning: Session IDs are reused after a logoff. Always re-run quser immediately before logoff – never reuse an ID you read five minutes ago.

What these commands do

All of them talk to the Remote Desktop Services stack (the TermService service) through RPC. That is worth remembering: the traffic does not use port 3389. A server that accepts RDP connections can still refuse quser /server: if RPC is filtered, and the reverse is also true.

CommandLong formWhat it answers
quserquery userWhich user accounts have a session, and how long they have been idle
qwinstaquery sessionEvery session slot on the host, including empty listeners and Services
rwinstareset sessionForce-terminates a session that will not close
logoffEnds a session gracefully, saving the profile
tsconAttaches an existing session to the console or to your own session
msgSends a pop-up to an active session before you disrupt it

Syntax

quser   [username | sessionname | sessionID] [/server:servername]
qwinsta [sessionname | username | sessionID] [/server:servername] [/mode] [/flow] [/connect] [/counter]
rwinsta {sessionname | sessionID}            [/server:servername] [/v]
logoff  [sessionname | sessionID]            [/server:servername] [/v]
ParameterApplies toMeaning
/server:allTarget host. Omit it and the command runs against the machine you are on.
/modeqwinstaCurrent line settings for the session.
/counterqwinstaTotal sessions created, disconnected and reconnected since boot.
/vrwinsta, logoffVerbose – prints what is being done. Useful inside scripts.

The STATE column is where the useful information lives. Active means a live connection. Disc means the user’s programs are still running but nobody is attached. Listen rows in qwinsta are the protocol listeners waiting for someone to connect – they are not people.


Practical examples

1. See who is on a server right now

The problem: A change window opens in ten minutes and you need to know whether anyone is still working on SRV-PROD-01.

The solution: quser lists only real user sessions, with idle time and logon time, so you can tell an active worker from a forgotten window at a glance.

rem /server: works from your own workstation - no need to RDP in first
quser /server:SRV-PROD-01

Expected output. The > in the left margin marks your own session, so it only appears when you run the command locally:

 USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
>administrator         console             1  Active      none   9/2/2026 8:12 AM
 jdoe                  rdp-tcp#2           2  Active         5   9/2/2026 9:40 AM
 msmith                                    3  Disc        1:22   9/1/2026 4:05 PM
 svc_backup            rdp-tcp#7           4  Active   1+02:14   9/1/2026 11:03 PM

Read the IDLE TIME column carefully – it uses three different formats. none means active in the last minute, a bare number is minutes, 1:22 is hours and minutes, and 1+02:14 is one day, two hours, fourteen minutes. That last format is your signal that a session has been abandoned rather than paused.

2. Find out why nobody else can connect

The problem: A colleague reports “the server is full” when connecting to SRV-PROD-01, but quser shows only two users and one of them is you.

The solution: Switch to qwinsta, which shows every session slot on the host rather than only the ones with a named user attached.

rem qwinsta shows listeners and the Services session that quser hides
qwinsta /server:SRV-PROD-01
 SESSIONNAME       USERNAME                 ID  STATE   TYPE        DEVICE
 services                                    0  Disc
>console           administrator             1  Active
 rdp-tcp#2         jdoe                      2  Active  rdpwd
                   msmith                    3  Disc
 rdp-tcp                                 65536  Listen

Session 0 (services) and the rdp-tcp listener at 65536 are infrastructure, not people. The real finding is msmith at ID 3: a disconnected session with no session name, occupying one of the two administrative slots.

Note: On a full Remote Desktop Session Host the limit comes from licensing, not from the two-session administrative cap. qwinsta /server:SRV-RDS-01 /counter shows how many sessions have been created and reconnected since boot, which is a faster health check than counting rows.

3. Warn the user, then release the session

The problem: You have identified msmith at ID 3 as the blocker, but that session may hold unsaved work.

The solution: Send a message first, wait, then use logoff, which closes processes properly and writes the user profile back to disk.

rem /time:30 keeps the pop-up on screen for 30 seconds instead of the 60-second default
msg 3 /server:SRV-PROD-01 /time:30 "Save your work - this session closes in 2 minutes."

rem timeout gives the user a real chance to react; /t is seconds, /nobreak ignores keypresses
timeout /t 120 /nobreak

rem /v prints what logoff is doing, which is what you want in a change log
logoff 3 /server:SRV-PROD-01 /v
Common mistake: msg only reaches sessions in Active state. Sending it to a Disc session succeeds silently and nobody ever sees it. If the session is disconnected, the user is not at the screen – contact them another way, or accept the risk.

4. The session that refuses to log off

The problem: You ran logoff 3, it returned without an error, and quser still shows ID 3 two minutes later. A hung application is refusing to exit and the graceful shutdown is stuck waiting for it.

The solution: rwinsta tears the session down at the Terminal Services layer without asking processes to co-operate.

rem confirm the ID is still what you think it is - IDs get recycled
quser /server:SRV-PROD-01

rem reset by ID; /v confirms the action rather than failing silently
rwinsta 3 /server:SRV-PROD-01 /v
Common mistake: rwinsta is not a stronger logoff – it is the equivalent of pulling the plug on that session. Unsaved data is lost and the roaming profile may not be written back. Use logoff first, every time, and keep rwinsta for sessions that have already ignored it.

5. Sweep a list of servers in one pass

The problem: Before a monthly patch run you need to know who is logged on across a handful of hosts, not just one.

The solution: A short batch file that builds the host list, then loops quser over it. Save the following as C:\bat\session-sweep.bat and run it from an elevated prompt.

@echo off
if not exist C:\bat mkdir C:\bat

rem build the host list once - the parentheses group the echoes into a single redirect,
rem and there is no space before each & so no trailing spaces end up in the file
(echo SRV-PROD-01&echo SRV-PROD-02&echo SRV-RDS-01)>C:\bat\servers.txt

rem %%s is the batch-file form of the loop variable - at an interactive prompt use %s instead
for /f %%s in (C:\bat\servers.txt) do (
    echo === %%s
    rem quser writes "No User exists for *" to the error stream, so 2>nul keeps output clean
    quser /server:%%s 2>nul
)
=== SRV-PROD-01
 USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
 jdoe                  rdp-tcp#2           2  Active         5   9/2/2026 9:40 AM
=== SRV-PROD-02
=== SRV-RDS-01
 USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
 msmith                                    3  Disc        1:22   9/1/2026 4:05 PM

SRV-PROD-02 printed nothing, which means nobody is logged on. A host that is unreachable produces the same empty result once 2>nul hides the error, so drop the redirect while you are troubleshooting connectivity.


Hidden gems

The blank column that breaks scripts

This is the single most common bug in quser automation. A disconnected session has an empty SESSIONNAME field, so splitting the line on whitespace returns five fields instead of six. Every value after the username shifts one place to the left, and the script happily records a session ID of Disc without complaining.

Look again at the output from example 1: msmith has nothing between the username and the ID. Any parser that assumes a fixed field count will mis-read that row, and it will be exactly the row you care about, because disconnected sessions are the ones you are hunting. The working PowerShell parser is in the next section.

Disc does not mean logged off

A session in Disc state is still fully alive. Its processes run, its memory is committed, its file handles are held and its network drives stay mapped. Users assume closing the RDP window ends the session; it does not. This is why a server can be short of RAM with nobody visibly logged on, and why a file stays locked long after its owner went home.

Reattaching a session instead of ending it

tscon moves an existing session to a different one rather than closing it. The practical use is rescuing your own session after a network drop: connect, find your old disconnected ID, and pull it to the session you are sitting in – your applications are exactly where you left them.

rem 2 is the old disconnected session; /dest is the session you are in right now
tscon 2 /dest:rdp-tcp#5
Warning: tscon only works without a password when the target session belongs to you. Connecting to another user’s session requires their password via /password:, and you cannot connect to the console session at all.

Query one user instead of the whole host

Both commands accept a filter as the first positional argument, which is far cheaper than listing everything and grepping. On a busy RDS host with a hundred sessions the difference is immediately noticeable.

rem returns only msmith's session, or an error if that user has none
quser msmith /server:SRV-RDS-01

PowerShell equivalents

There is no built-in cmdlet that replaces quser on a plain Windows install – Get-RDUserSession exists only on a Remote Desktop deployment with the RDS module. The practical approach is to keep using quser and turn its text output into objects. The function below handles the blank-column case described above.

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

    # quser is a native exe - 2>$null swallows "No User exists for *" on an idle host
    $raw = if ($ComputerName -eq $env:COMPUTERNAME) { quser 2>$null }
           else { quser /server:$ComputerName 2>$null }

    if (-not $raw) { return }

    $raw | Select-Object -Skip 1 | ForEach-Object {
        # strip the '>' marking your own session, then split on runs of 2+ spaces
        $f = ($_ -replace '^>', ' ').Trim() -split '\s{2,}'

        # a disconnected session has an EMPTY SESSIONNAME -> only 5 fields, not 6
        if ($f.Count -eq 6) { $sess = $f[1]; $rest = $f[2..5] }
        else                { $sess = '';    $rest = $f[1..4] }

        [pscustomobject]@{
            Server      = $ComputerName
            UserName    = $f[0]
            SessionName = $sess
            Id          = [int]$rest[0]
            State       = $rest[1]
            IdleTime    = $rest[2]
            LogonTime   = $rest[3]
        }
    }
}

With sessions as objects you can filter the way you would with any other cmdlet. This finds sessions that have been disconnected for more than a day – the + only appears in the idle-time format once it passes 24 hours, which makes it a reliable marker for abandonment.

Get-RdpSession -ComputerName SRV-RDS-01 |
    Where-Object { $_.State -eq 'Disc' -and $_.IdleTime -match '\+' } |
    Format-Table Server, UserName, Id, IdleTime, LogonTime -AutoSize

And this closes them. Wrap $_.Id in $( ) so PowerShell evaluates the property before handing the value to logoff as an argument.

Get-RdpSession -ComputerName SRV-RDS-01 |
    Where-Object { $_.State -eq 'Disc' -and $_.IdleTime -match '\+' } |
    ForEach-Object {
        Write-Host "Logging off $($_.UserName) (session $($_.Id))"
        logoff $($_.Id) /server:SRV-RDS-01
    }

For many hosts at once, run quser locally on each one through PowerShell remoting rather than making a remote RPC call per server. One round trip per host is faster and it works where RPC is filtered but WinRM is not.

$servers = 'SRV-PROD-01','SRV-PROD-02','SRV-RDS-01'

# the ScriptBlock runs on the far end, so quser reports on its own local sessions
Invoke-Command -ComputerName $servers -ScriptBlock { quser 2>$null } |
    ForEach-Object { '{0,-14} {1}' -f $_.PSComputerName, $_ }

WMI offers a second, independent view. Win32_LogonSession with LogonType 10 returns RemoteInteractive logons, which is useful for correlating against event logs.

# LogonType 10 = RemoteInteractive (RDP), 2 = Interactive (console)
$logons = Get-CimInstance Win32_LoggedOnUser

Get-CimInstance Win32_LogonSession -Filter 'LogonType = 10' | ForEach-Object {
    $s = $_
    $u = $logons | Where-Object { $_.Dependent.LogonId -eq $s.LogonId } | Select-Object -First 1
    [pscustomobject]@{
        Account   = '{0}\{1}' -f $u.Antecedent.Domain, $u.Antecedent.Name
        LogonId   = $s.LogonId
        StartTime = $s.StartTime
    }
}
Warning: Win32_LogonSession lists logon sessions, not Terminal Services sessions. There is no session ID in that output that you can pass to logoff, and a single user can hold several logon sessions at once. Use it for auditing and correlation – keep quser for anything you intend to act on.

Where this matters

  • Before a reboot. One quser /server: tells you whether the maintenance window is actually clear, without opening a single RDP connection.
  • When the two-session limit bites. A disconnected admin session from last week is the usual reason a server refuses your connection at the worst possible moment.
  • Chasing a locked file. If a document or database file will not release, the owning session is often disconnected rather than gone – quser finds it in seconds.
  • Unexplained memory pressure. A handful of forgotten Disc sessions on an RDS host can hold gigabytes that Task Manager attributes to nothing obvious.
  • Security review. Cross-checking live sessions against expected working hours exposes shared accounts and stale service logons faster than reading event logs.
  • Recovering your own work. After a VPN drop, tscon reattaches the session you lost instead of forcing you to start over.

Tips and limitations

  • You can always query and log off your own session. Acting on another user’s session requires Full Control permission on that session – in practice, local administrator on the target.
  • /server: uses RPC, not port 3389. If it fails with an access or RPC error while RDP itself works, look at the firewall and at RPC rather than at Remote Desktop settings.
  • Windows Home editions do not ship quser or qwinsta. On Pro, Enterprise and Server they are in %SystemRoot%\System32 and need no installation.
  • You cannot log off the console session, and you cannot tscon into it.
  • Session IDs are recycled. Always re-query immediately before you act, especially inside scripts that loop over several hosts.
  • msg reaches active sessions only, and delivery is not guaranteed – treat it as a courtesy, not as consent.
  • Column widths in quser output shift with long usernames. Split on runs of two or more spaces rather than on fixed character positions.

Official documentation


Related tools

  • Windows Event Log Analyzer – paste the logon and session events you find while auditing sessions and get the event ID explained in context.
  • Port Checker – confirm whether 3389 is actually reachable before blaming session limits for a failed connection.

Related guides