October 13, 2026
The RDP Bug That Only Struck When Someone Closed Their Window
Two outages, one Windows update, and the debugging habits I’m keeping after “Remote Desktop Services is currently busy”
By Prateek Srivastava
8 min read
The message is polite. That's what makes it annoying.
The task you are trying to do can't be completed because Remote Desktop Services is currently busy. Please try again in a few minutes. Other users should still be able to log on.
We tried again in a few minutes. Then a few hours. Other users could not log on either.
The server was a Windows Server 2016 VM on Azure that runs SSIS batch jobs for a client's pre-production environment. It went down like this twice in under two weeks. The first time cost about 22 hours of RDP access. The second time cost about 45. Both times, the only thing that brought it back was stopping and starting the VM.
The fix turned out to be one Microsoft update. But the path there taught me more than the fix did, so here's the whole story, including the parts where I was wrong.
Day one: chasing the wrong number
When a server refuses logins, you open the metrics. Ours showed available memory at about 13%. CPU at 6%.
Thirteen percent free looks scary, so I spent the first hour on memory. SQL Server on the box? SSIS packages holding RAM? I had a theory ready.
Then I looked at the actual numbers instead of the percentage. The VM had 64 GB of RAM. Thirteen percent is about 7.5 GB free, which is perfectly healthy for a box running SQL Server. Disk queue was near zero. Nothing was paging.
Lesson one: a scary-looking metric is not a root cause. Convert percentages into real numbers before you build a theory on them.
The real clue came from somewhere else. I tried Azure Run Command to look inside the VM, and it hung. The guest agent was healthy and still sending heartbeats, but it couldn't start the extension it needed. Something inside Windows had stopped working while the machine itself looked fine.
We needed the server back, so we restarted it. RDP worked again. No root cause. That bothered me.
Reading the logs like a crime scene
After the restart I went back through the event logs for the outage window. To make sense of them, you need to know how an RDP connection is actually built, because every stage writes its own event.
The important part is step 4, session arbitration. Windows asks: does this user already have a session? If yes, reconnect them to it. If no, log them on fresh. A healthy arbitration starts with Event 41 and ends with Event 42, usually within a second.
Here's the last good connection, and the one right after it:
08:36:39 Event 25 Session reconnection succeeded ← last success
08:39:05 Event 41 Begin session arbitration (Session 4)
08:39:06 Event 24 Session has been disconnected
... no Event 25, no Event 42. Ever.08:36:39 Event 25 Session reconnection succeeded ← last success
08:39:05 Event 41 Begin session arbitration (Session 4)
08:39:06 Event 24 Session has been disconnected
... no Event 25, no Event 42. Ever.Forty-seven seconds later, the System log started filling up:
08:39:52 Event 7011 A timeout (30000 milliseconds) was reached while waiting
for a transaction response from the iphlpsvc service.
08:43:22 Event 7011 ... from the gpsvc service.
08:44:22 Event 7046 Group Policy Client has repeatedly stopped responding.08:39:52 Event 7011 A timeout (30000 milliseconds) was reached while waiting
for a transaction response from the iphlpsvc service.
08:43:22 Event 7011 ... from the gpsvc service.
08:44:22 Event 7046 Group Policy Client has repeatedly stopped responding.Event 7011 means the Service Control Manager sent a service a request and got no answer in 30 seconds. Over the next eight minutes, nine services did the same thing: IP Helper, Group Policy Client, DNS Client, Network Location Awareness, Cryptographic Services and more.
Nine broken services sounds like chaos. It wasn't. When I checked which process hosts each one, they all lived in just two shared svchost processes.
Get-CimInstance Win32_Service |
Where PathName -match '-k (netsvcs|NetworkService)\b' |
Select Name, State, @{n='Group';e={($_.PathName -split '-k ')[1]}}Get-CimInstance Win32_Service |
Where PathName -match '-k (netsvcs|NetworkService)\b' |
Select Name, State, @{n='Group';e={($_.PathName -split '-k ')[1]}}On our Server 2016 box, the netsvcs process also hosts the User Profile Service, User Manager and Remote Desktop Configuration. Those are exactly the services RDP needs to build a session. When that process stopped answering, every new connection got stuck at step 4. Authentication still succeeded (Event 1149) every single time. The password was never the problem.
I also ruled things out one by one: network, accounts, CPU, memory, disk, Azure platform health. All clean. And there were zero service timeouts between the last reboot and that morning.
So I knew exactly what broke. I had no idea why. That's an uncomfortable thing to write in an RCA, but it's better than guessing.
It happened again, and this time it left a fingerprint
Nine days later, same message, same server. This time I knew where to look, and the timeline was almost identical. With one extra line:
12:33:25 Event 24 Session 4 has been disconnected
12:33:55 Event 7011 Timeout waiting for the iphlpsvc service ← exactly 30 s later
12:34:25 Event 6005 The winlogon notification subscriber <SessionEnv>
is taking long time to handle the notification
event (Disconnect).12:33:25 Event 24 Session 4 has been disconnected
12:33:55 Event 7011 Timeout waiting for the iphlpsvc service ← exactly 30 s later
12:34:25 Event 6005 The winlogon notification subscriber <SessionEnv>
is taking long time to handle the notification
event (Disconnect).SessionEnv is Remote Desktop Configuration. Windows was telling me, in plain English, that this service got stuck handling a disconnect.
I went back to the first outage. It also started right after a user disconnected a session. Somebody closed their RDP window, and the server stopped accepting new ones.
Users disconnect dozens of times a day, though, so the disconnect couldn't be the whole story. Something had made disconnects dangerous.
The answer was a known issue all along
I opened a case with Microsoft and sent the timeline. The reply was short: there's a known issue with Remote Desktop Services in the September 2026 cumulative updates. For Server 2016, install KB5129239.
I checked the server:
OS build: 14393.9512
KB5123099 Security Update installed 19 SepOS build: 14393.9512
KB5123099 Security Update installed 19 SepKB5123099 is the September cumulative update for Server 2016. Our monthly patching had installed it on 19 September, nine days before the first outage.
The publicly reported symptoms matched what I'd seen almost line for line: servers stuck at "Please wait for the Remote Desktop Configuration", sessions not disconnecting cleanly, new connections hanging, some servers needing a hard reset, and a VM restart only helping temporarily.
I'd spent two days building a careful evidence trail. Ten minutes on the known-issues page for that update would have pointed me to the fix. That stung a bit. The evidence wasn't wasted, though: it's what let Microsoft confirm the match quickly, and it's what I'd need if the patch hadn't worked.
Installing the fix was its own small adventure
Two mistakes worth sharing, because both are easy to make.
Mistake one: I downloaded the wrong KB. The email mentioned both the broken update and the fix. I grabbed KB5123099 first, the one that caused the problem. Windows politely told me it was already installed. Read KB numbers twice.
Mistake two: I trusted the installer. The first install of KB5129239 said "successfully installed". After the reboot, the build was still 14393.9512. Windows Update history told the truth:
Update for Windows (KB5129239) Result: Failed HResult: 0x800F0841Update for Windows (KB5129239) Result: Failed HResult: 0x800F0841The second attempt worked, but it took over an hour, most of it on a "Getting Windows ready, don't turn off your computer" screen. If you don't have Boot diagnostics enabled on your Azure VM (we didn't), you'll be staring at a dead RDP session wondering whether it's working. I enabled it during the install. It should have been on from day one.
The only check that matters is the build number:
"OS build: 14393." + (Get-ItemProperty `
'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR
# Before: 14393.9512 After: 14393.9514"OS build: 14393." + (Get-ItemProperty `
'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR
# Before: 14393.9512 After: 14393.9514Watching an update you can't log in to
The worst part of that hour wasn't the waiting. It was not knowing whether anything was happening. RDP refused every login while Windows finished the update, and a frozen-looking spinner tells you nothing.
Azure Run Command doesn't need RDP. It runs a script inside the VM through the guest agent, as long as the agent is up. So from Cloud Shell, I wrote a tiny progress check:
cat > prog.ps1 <<'EOF'
"Boot: " + (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
"Build: 14393." + (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR
Get-Process TiWorker -EA SilentlyContinue | Select Name, @{n='CPUsec';e={[math]::Round($_.CPU)}} | Out-String
Get-Content C:\Windows\Logs\CBS\CBS.log -Tail 3
EOF
az vm run-command invoke -g <resource-group> -n <vm-name> \
--command-id RunPowerShellScript --scripts @prog.ps1 \
--query "value[].message" -o tsvcat > prog.ps1 <<'EOF'
"Boot: " + (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
"Build: 14393." + (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR
Get-Process TiWorker -EA SilentlyContinue | Select Name, @{n='CPUsec';e={[math]::Round($_.CPU)}} | Out-String
Get-Content C:\Windows\Logs\CBS\CBS.log -Tail 3
EOF
az vm run-command invoke -g <resource-group> -n <vm-name> \
--command-id RunPowerShellScript --scripts @prog.ps1 \
--query "value[].message" -o tsvFour lines, each with a job:
Boot time tells you whether the restart has actually happened yet. Build number tells you whether the update has landed. TiWorker CPU is the Windows Modules Installer worker, the process that applies updates. If its CPU time keeps rising between checks, it's working. The last lines of CBS.log show what servicing is doing right now, with timestamps.
I ran it every few minutes. This is what progress looked like:
08:01 TiWorker 1968 s CBS: Appl: detect ... ← working out what applies
08:09 TiWorker 2446 s CBS: Appl: detect ...
08:13 TiWorker 2701 s CBS: Exec: Install package ... ← actually installing
08:21 Boot: 08:17:23 Build: 14393.9514 ← done08:01 TiWorker 1968 s CBS: Appl: detect ... ← working out what applies
08:09 TiWorker 2446 s CBS: Appl: detect ...
08:13 TiWorker 2701 s CBS: Exec: Install package ... ← actually installing
08:21 Boot: 08:17:23 Build: 14393.9514 ← doneRising CPU and fresh log timestamps mean leave it alone. If both stop moving for a long time, that's when you start worrying, and even then, talk to support before you force a restart in the middle of servicing. Interrupting an update is a good way to turn one problem into two.
You may also see the old, outgoing update's KB number appear in CBS.log during the install. That's normal: the new cumulative update replaces the previous one, so Windows works through the old one's packages too.
What I do differently now
1. Read the known issues after every patch cycle. Every Windows cumulative update has a support page with a "Known issues" section, and Microsoft's Windows release health dashboard collects them. Check it after patching, and again when something odd starts a week later. "What changed recently?" should be the first question in any incident, not the last.
2. Put the patch date on the incident timeline. I had the patch date in my logs from day one. I just didn't line it up with the outage until Microsoft did.
3. Don't restart first if you can help it. The restart is what destroys the evidence. Export the event logs, note the times, and if you can, capture a memory dump of the hung process. On Azure, enabling Boot diagnostics and Serial Console before you need them gives you a way in when RDP is gone.
4. Verify updates by build number, not by dialog box. "Installed successfully" can still end in a rollback.
5. Have a way in that doesn't need RDP. On Azure that's Run Command, Serial Console and Boot diagnostics. When RDP is the thing that's broken, these are how you see what's going on and decide whether to wait or act.
6. Learn the handful of event IDs that matter. These told me more than any dashboard did:
And here's the snippet I now run first whenever RDP acts up. It finds the moment things broke and the last connection that worked:
$since = (Get-Date).AddDays(-2)
$lsm = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
# When did services stop answering?
Get-WinEvent -FilterHashtable @{LogName='System'; Id=7011,7046; StartTime=$since} -EA SilentlyContinue |
Sort TimeCreated | Select -First 10 TimeCreated, Message
# Last successful logon/reconnect
Get-WinEvent -FilterHashtable @{LogName=$lsm; Id=21,25; StartTime=$since} -MaxEvents 3 |
Select TimeCreated, Id, Message
# Stuck arbitrations: 41s without matching 42s
Get-WinEvent -FilterHashtable @{LogName=$lsm; Id=41,42; StartTime=$since} |
Sort TimeCreated | Select TimeCreated, Id, Message$since = (Get-Date).AddDays(-2)
$lsm = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
# When did services stop answering?
Get-WinEvent -FilterHashtable @{LogName='System'; Id=7011,7046; StartTime=$since} -EA SilentlyContinue |
Sort TimeCreated | Select -First 10 TimeCreated, Message
# Last successful logon/reconnect
Get-WinEvent -FilterHashtable @{LogName=$lsm; Id=21,25; StartTime=$since} -MaxEvents 3 |
Select TimeCreated, Id, Message
# Stuck arbitrations: 41s without matching 42s
Get-WinEvent -FilterHashtable @{LogName=$lsm; Id=41,42; StartTime=$since} |
Sort TimeCreated | Select TimeCreated, Id, MessageThe part I keep thinking about
The logs were good enough to show exactly which process hung, when, and which user action set it off. They could never have told me why, because the why was a bug in code I don't own.
That's the balance in infrastructure work. Do the investigation properly, because sometimes it's the only way to a fix. But check what changed and what's already known before you go deep. Sometimes the answer has been sitting on a support page for weeks.
If you run Windows Server and patched in September, it's worth five minutes to check your build number.
If this saved you time — or if you've been bitten by a patch that broke something unrelated? I'd like to hear how you caught it— drop it in the comments . I publish one production incident breakdown every week. Subscribe free if you want it directly in your inbox — no tutorials, no fluff:
Let's Stay in Touch 🪂 Connect with us on: LinkedIn, Instagram, Medium