
Automatically Remove Stale User Profiles on an RDS Host
On a busy Remote Desktop Session Host, local profiles pile up fast — every user who ever logged in leaves a folder under C:\Users, and a system drive fills quietly until logons start failing. This guide sets up an unattended, standard-supported cleanup of profiles that haven't been used in N days.
I assume Windows Server 2019 or 2022 running the RDS Session Host role, with Windows PowerShell 5.1 (the in-box version). I assume local, roaming, or mandatory profiles stored under C:\Users — not FSLogix profile containers or User Profile Disks. If you use FSLogix or UPD, stop here: those are managed by their own mechanisms and this method does not apply.
Before you run this
What it does: it enumerates local user profiles via the Win32_UserProfile class, finds those whose LastUseTime is older than a threshold you set, and deletes them properly — folder and the associated registry entry under ProfileList. Deleting the profile folder by hand leaves orphaned registry keys behind; the CIM delete method removes both, which is why we use it.
Privileges: this must run in an elevated PowerShell session (Run as administrator), and the scheduled task must run as SYSTEM or a local administrator. Deleting profiles touches C:\Users and HKLM.
This is destructive and irreversible. Deleting a profile removes that user's local documents, desktop, cached data, and per-user registry hive (NTUSER.DAT). There is no built-in undo. If the profile is roaming, the server copy of unsynced data is gone. Treat this with the caution you'd give any bulk delete.
Test first. Read the script. Run it on a lab RDS host or a spare VM before it ever touches a production server, and on the real box run it in preview mode (-WhatIf) first and review the list of profiles it would delete. LastUseTime is a real property but it is not a perfect record of "last interactive logon" in every scenario — verify the candidates look right before you commit.
A cleanup run is also a good moment to confirm you have a current backup or a VSS snapshot / VM checkpoint of the system drive, since that is your only recovery path if you delete something you shouldn't have.
The safe approach: the built-in filters
Win32_UserProfile gives us three properties that keep us out of trouble:
Special—$truefor system accounts (LocalSystem, NetworkService, and so on). Never delete these.Loaded—$truewhen the profile is currently in use. We skip loaded profiles so we never yank a profile from an active session.LastUseTime— aDateTime(when read viaGet-CimInstance; the olderGet-WmiObjectreturns a string that needs conversion, which is one reason we use CIM).
The script
Save this as C:\Scripts\Remove-StaleProfiles.ps1. Change the default -Days and -LogFile to suit you. It is safe by default: because it uses SupportsShouldProcess, running it with -WhatIf previews and deletes nothing.
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[int]$Days = 30, # profiles idle longer than this are candidates
[string]$LogFile = 'C:\Scripts\ProfileCleanup.log'
)
$cutoff = (Get-Date).AddDays(-$Days)
"{0} Run start. Cutoff = {1} (Days = {2})" -f (Get-Date), $cutoff, $Days |
Tee-Object -FilePath $LogFile -Append
# Select only non-system, not-currently-loaded profiles older than the cutoff.
$stale = Get-CimInstance -ClassName Win32_UserProfile | Where-Object {
-not $_.Special -and # skip system/service profiles
-not $_.Loaded -and # skip profiles in active use
$null -ne $_.LastUseTime -and # skip profiles with no usable timestamp
$_.LastUseTime -lt $cutoff
}
foreach ($p in $stale) {
if ($PSCmdlet.ShouldProcess($p.LocalPath, "Delete profile (last used $($p.LastUseTime))")) {
try {
Remove-CimInstance -InputObject $p -ErrorAction Stop # removes folder AND ProfileList entry
"{0} DELETED {1} (lastuse {2})" -f (Get-Date), $p.LocalPath, $p.LastUseTime |
Tee-Object -FilePath $LogFile -Append
}
catch {
"{0} ERROR {1} {2}" -f (Get-Date), $p.LocalPath, $_.Exception.Message |
Tee-Object -FilePath $LogFile -Append
}
}
}
"{0} Run complete." -f (Get-Date) | Tee-Object -FilePath $LogFile -Append
Replace C:\Scripts\... with wherever you keep scripts and logs. -Days 30 is a placeholder threshold — pick a number that matches how long a user can be away before you're comfortable wiping their local profile.
Preview, then commit
From an elevated prompt, always preview first:
# Shows exactly which profiles WOULD be deleted, changes nothing
C:\Scripts\Remove-StaleProfiles.ps1 -Days 30 -WhatIf
Read that list. When you're satisfied, run it for real:
C:\Scripts\Remove-StaleProfiles.ps1 -Days 30
Scheduling it
Once you trust the script, run it on a schedule as SYSTEM. These ScheduledTasks module cmdlets are standard on Server 2019/2022. Run this once, elevated:
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Remove-StaleProfiles.ps1" -Days 30'
$trigger = New-ScheduledTaskTrigger -Daily -At '03:00' # off-hours, no active sessions
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' `
-LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Remove Stale RDS Profiles' `
-Action $action -Trigger $trigger -Principal $principal `
-Description 'Deletes local user profiles idle longer than 30 days'
Note there is no -WhatIf in the scheduled argument — the task performs real deletions. That's the point of previewing manually first. Schedule it for a maintenance window when no one is logged on, so Loaded profiles (which we skip anyway) are minimal.
The native alternative
Microsoft ships a Group Policy setting that does much the same thing without a script:
Computer Configuration → Policies → Administrative Templates → System → User Profiles → "Delete user profiles older than a specified number of days on system restart."
It runs at restart rather than on a schedule and uses its own "last used" determination, which can differ slightly from LastUseTime. If you already reboot session hosts regularly and don't need logging or dry-run control, this is the lower-maintenance option. The PowerShell approach wins when you want a log, a preview, and cleanup without a reboot.
Verify it worked
Check the log first:
Get-Content C:\Scripts\ProfileCleanup.log -Tail 40
Then confirm the profiles are actually gone — both from CIM and from disk:
# Remaining non-system profiles and their last-use dates
Get-CimInstance Win32_UserProfile |
Where-Object { -not $_.Special } |
Select-Object LocalPath, LastUseTime, Loaded |
Sort-Object LastUseTime
# The folders that remain
Get-ChildItem C:\Users
Because we used the CIM delete method, the matching entries under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList are removed too — you can spot-check that key in regedit if you want to be sure no orphans remain.
Undo
There is no rollback for a deleted profile. If you deleted one you needed, your options are recovery from your backup, from a VSS previous version of the folder if shadow copies are enabled, or from a VM checkpoint taken before the run. This is exactly why the preview step and a current backup are non-negotiable.
For exact property definitions and method behaviour, see the Microsoft Learn documentation for the Win32_UserProfile class, the Remove-CimInstance cmdlet, and Register-ScheduledTask before adapting any of this to your environment.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Automate DHCP Lease Reporting on Windows Server
This guide builds a read-only PowerShell script that enumerates your DHCP scopes, pulls the current IPv4 leases from each, writes them to a timestamped CSV, and (optionally) emails the file. It does not change any DHCP configuration — no scopes, reservations, options, or leases are created, modified, or deleted. The worst it can do is fill a disk with CSVs if you never prune them.
Automate a Daily Failed-Logon (4625) Report with PowerShell
This guide builds a scheduled PowerShell job that reads Event ID 4625 (failed logon) from the Windows Security log for the last 24 hours and writes them to a dated HTML report. It is read-only — it queries the event log and creates a report file. It does not change auditing policy, delete events, or touch accounts.
Monitor Free Disk Space and Email an Alert with PowerShell
This script reads the free space on the fixed local disks of the machine it runs on and emails you when any of them drops below a percentage you set. It is a lightweight stand-in for a monitoring platform — good for a handful of servers, not a replacement for one across a fleet.
Automate a Nightly Robocopy Mirror to a NAS with Verification
This sets up a nightly one-way mirror of a local folder to a NAS SMB share using robocopy /MIR , followed by a second pass that lists any remaining differences as a verification step, all logged to a timestamped file and driven by a Scheduled Task.




