Auto-Shutdown EC2 Windows Server When Idle
How I built a PowerShell script + Windows Task Scheduler setup to automatically shut down an EC2 Windows when no RDP sessions are active.
The Problem
Running an EC2 Windows Server 24/7 is expensive and unnecessary if you only use it occasionally via RDP. Every hour it sits idle costs money and keeps ports exposed to the internet.
The goal was simple: automatically shut down the server when no one is connected via RDP.
The Solution
A PowerShell script that:
- Queries active RDP sessions on the server
- If sessions exist — lists them and does nothing
- If no sessions exist — shuts the server down
Triggered automatically by Windows Task Scheduler on two events:
- When an RDP user disconnects
- On system startup (to handle auto-reboots from Windows Update)
The Script
<# auto-shutdown.ps1 #>
#Requires -RunAsAdministrator
$LogFile = "$PSScriptRoot\RDP-Shutdown.log"
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$timestamp] $Message"
Write-Host $line
Add-Content -Path $LogFile -Value $line
}
Write-Log "=== RDP check started ==="
$raw = query session 2>&1
$activeSessions = @()
foreach ($line in $raw) {
if ($line -match "SESSIONNAME") { continue }
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$cleaned = ($line -replace "^[^a-zA-Z0-9]+", "")
if ($cleaned -match "^rdp-tcp" -and $cleaned -match "Active") {
$parts = $cleaned -split "\s+"
$activeSessions += [PSCustomObject]@{
SessionName = $parts[0]
UserName = if ($parts.Count -ge 2) { $parts[1] } else { "unknown" }
SessionID = if ($parts.Count -ge 3) { $parts[2] } else { "?" }
State = "Active"
}
}
}
if ($activeSessions.Count -gt 0) {
$tableString = $activeSessions | Format-Table -AutoSize | Out-String
Write-Log $tableString
Write-Log "Active RDP session(s) found - shutdown aborted."
} else {
Write-Log "No active RDP sessions found. Shutting down..."
shutdown.exe /s /t 0 /f
}Line-by-Line Explanation
Setup
#Requires -RunAsAdministratorEnforces that the script must run as Administrator. Required to query sessions and execute shutdown.
$LogFile = "$PSScriptRoot\RDP-Shutdown.log"$PSScriptRoot is the folder where the script lives. The log file is created there automatically.
Logging
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$timestamp] $Message"
Write-Host $line
Add-Content -Path $LogFile -Value $line
}A reusable function that prints to the console and appends to the log file simultaneously. Every action is timestamped, making it easy to audit when shutdowns happened and why.
Querying Sessions
$raw = query session 2>&1query session is a built-in Windows command that lists all sessions — RDP, console, and system services. 2>&1 merges stderr into stdout so any errors are captured rather than silently lost.
A typical output looks like this:
SESSIONNAME USERNAME ID STATE TYPE
services 0 Disc
console 1 Conn
>rdp-tcp#0 Administrator 2 Active
rdp-tcp 65537 Listen
The > at the start of a line means that is the current session (the one running the script).
Parsing the Output
$cleaned = ($line -replace "^[^a-zA-Z0-9]+", "")This regex strips all leading characters that are not letters or numbers. This is the key fix — Windows prefixes the current session line with > and spaces, which would otherwise break the match. After cleaning, >rdp-tcp#0 becomes rdp-tcp#0.
if ($cleaned -match "^rdp-tcp" -and $cleaned -match "Active") {Two conditions must both be true:
- Line starts with
rdp-tcp— meaning it is a Remote Desktop session (not console or a system service) - Line contains
Active— meaning someone is currently connected, not just a listening socket
$parts = $cleaned -split "\s+"Splits the cleaned line on whitespace to extract individual columns: session name, username, ID, and state.
Decision
if ($activeSessions.Count -gt 0) {
$tableString = $activeSessions | Format-Table -AutoSize | Out-String
Write-Log $tableString
Write-Log "Active RDP session(s) found - shutdown aborted."
} else {
Write-Log "No active RDP sessions found. Shutting down..."
shutdown.exe /s /t 0 /f
}If any active RDP sessions were found, the script prints + logs them in a table and exits safely.
If no sessions were found, it calls:
/s— shutdown (not restart)/t 0— no additional built-in delay/f— force-close any open applications
Task Scheduler Setup
The script alone does nothing on its own — it needs to be triggered automatically. Windows Task Scheduler handles this. Here is how to set it up from scratch.
Opening Task Scheduler
Press Win + R, type taskschd.msc and hit Enter. In the top bar click
Action > Create Task (not "Create Basic Task" — you need the full options).
General Tab
This is where you configure who runs the task and with what permissions.
- Name: Give it something descriptive, e.g.
Check Active RDP - Auto Shutdown - Select:
Run whether user is logged on or not- This ensures the task fires even if no one is actively logged in at the time
- Check:
Run with highest privileges- The script calls
shutdown.exeandquery session, both of which require elevated permissions. Without this the script will silently fail.
- The script calls
- Configure for: Match your Windows Server version (e.g. Windows Server 2025)
Triggers Tab — Two Triggers
Click New to add each trigger.
Trigger 1: On RDP Disconnect
This is the primary trigger. It fires the moment a user closes their RDP window (or gets disconnected due to a network drop).
Settings:
- Begin the task:
On disconnect from user session - Any user or scope it to a specific user if needed
- Leave delay unchecked — you want this to fire immediately
Trigger 2: At Startup (5-minute delay)
This covers an edge case that would otherwise leave the server running forever: automatic reboots triggered by Windows Update, a crash, or any other reason that restarts the machine without a human initiating it.
Without this trigger, the server boots back up and just sits there running indefinitely because the disconnect trigger only fires when a user disconnects — not when the machine starts up cold.
The 5-minute delay is intentional. If you rebooted the server yourself and want to use it, this gives you enough time to RDP back in before the script runs. If you do reconnect within those 5 minutes, the script sees an active session and aborts the shutdown.
Settings:
- Begin the task:
At startup - Check:
Delay task for→ set to5 minutes
Actions Tab
This tells Task Scheduler what to actually run when triggered.
Click New and fill in:
- Action:
Start a program - Program/script:
powershell.exe - Add arguments:
-ExecutionPolicy Bypass -File "C:\Users\Administrator\Desktop\script.ps1"-ExecutionPolicy Bypass— overrides any system policy that would block unsigned scripts from running-File— tells PowerShell to run a specific script file rather than an inline command
- Start in:
C:\Users\Administrator\Desktop- This sets the working directory so
$PSScriptRootresolves correctly and the log file is created in the right place
- This sets the working directory so
⚠️ Do not set the action to run the
.ps1file directly. Always callpowershell.exeand pass the script as an argument — otherwise Task Scheduler may not execute it correctly.
Conditions Tab
- Uncheck:
Start the task only if the computer is on AC power(optional)- This option is meant for laptops or personal desktops.
- Leave everything else as default
Verify It Works
- After saving, you can right-click the task and hit Run to test it manually. Check the log file at the script location — it should show a timestamped entry confirming the script ran and either found active sessions or initiated a shutdown.
Gotchas I Hit Along the Way
Smart quotes breaking the script
Copy-pasting from formatted sources introduced curly quotes (") instead of straight quotes ("). PowerShell cannot parse these and throws a confusing terminator error. Fix: always write scripts in a plain text editor like VS Code, never Word or a browser textarea.
The > prefix on the current session
query session marks the current session with a > character. My initial regex ^rdp-tcp failed to match because the line actually started with >rdp-tcp. Fixed by stripping all leading non-alphanumeric characters before matching.
Result
The server now shuts itself down within 60 seconds of the last RDP user disconnecting, and recovers cleanly from unattended reboots. No more idle EC2 bills.