cd ../blogs
blog7 min read

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.

#aws#ec2#powershell#automation#devops

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:

  1. Queries active RDP sessions on the server
  2. If sessions exist — lists them and does nothing
  3. 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 -RunAsAdministrator

Enforces 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>&1

query 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.exe and query session, both of which require elevated permissions. Without this the script will silently fail.
  • 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 to 5 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 $PSScriptRoot resolves correctly and the log file is created in the right place

⚠️ Do not set the action to run the .ps1 file directly. Always call powershell.exe and 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.