Delete Old User Profiles in Windows

Delete Old User Profiles in Windows

Administrators should occasionally delete old user profiles (retired or inactive users, etc.) from C:Users on Windows workstations and servers. The Windows user profile cleanup task is most commonly performed on Remote Desktop Services (RDS) terminal servers.

The main problem with RDS servers is the constant growth in the size of the user profile directories on the hard disk. This problem is partially solved by user profile size quotas using FSRM or NTFS quotas, using roaming profiles such as FSLogix or User Profile Disk, redirected folders, etc. However, if you have a large number of RDS users, over time the C:Users folder will contain a large number of directories with old (unused) user profiles.

How to Delete a User Profile in Windows Manually?

In Windows, you can delete a profile manually from the Control Panel:

  1. Open the Advanced System Settings (run the command SystemPropertiesAdvanced ) and go to User Profiles -> Settings;
  2. This window lists all the user profiles (local, domain, and Microsoft accounts) stored on this computer. The size of each user profile on disk is listed in the Size column;
  3. Select the user whose profile you want to delete and click the Delete button.

On Windows 11/10 and Windows Server 2022/2019, you can delete user profiles from disk through the Settings app. Go to Accounts -> Access work and school (or run the URI shortcut ms-settings:otherusers ). Select a user and click Remove to delete their profile data from the computer.

ms-settings - remove user profile in windows 11

When a user profile is properly deleted in Windows, the profile directory in C:Users and the user entry in the registry are deleted.

Many novice administrators try to manually remove the user profile directory from the C:Users folder. In this case, you will need to manually delete the profile reference from the Windows registry:

  1. Run the Registry Editor (regedit.exe);
  2. Go to the registry key HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionProfileList;
  3. For each user logged in locally (this login method must be allowed for the user by the Allow log on locally GPO option), a separate sub-key is created with the user’s SID as the name;
  4. You can find the registry key corresponding to the user by its SID, or you can manually browse the contents of all subkeys until you find a key in which the ProfileImagePath value points to the directory with the user profile on disk (for example, C:Usersj.smith);profileimagepath in registry
  5. Delete this registry key to complete the correct removal of the profile.

You can also delete a specific user’s profile using PowerShell:

Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.LocalPath.split(‘’)[-1] -eq 'j.smith' } | Remove-CimInstance

This command removes both the hard drive directory and the j.smith user profile reference uthe nder HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionProfileList registry.

You can remove a user profile on a remote computer using PowerShell Remoting and the Invoke-Command cmdlet:

$compname="mun-wks92s3"
$user = "j.smith"
Invoke-Command -ComputerName $compname -ScriptBlock {
param($user)
Get-CimInstance -Class Win32_UserProfile | Where-Object { $_.LocalPath.split(‘’)[-1] -eq $user } | Remove-CimInstance
} -ArgumentList $user

GPO: Delete User Profiles Older Than a Specified Number of Days

In Windows, there is a built-in Group Policy option to automatically delete user profiles older than xx days. You can enable this option using the Local Group Policy Editor (gpedit.msc) or with the domain GPO management console (gpmc.msc). In this example, we are going to apply an automatic profile cleanup policy to hosts in the RDS farm that are in a separate container (Organizational Unit, OU) in Active Directory.

  1. Locate the OU containing the computers/servers to which you want to apply the user profile cleanup policy.  Right-click on the OU and select Create a GPO in this domain and Link it here;create new domain gpo
  2. Specify the policy name and edit the GPO;
  3. Navigate to Computer Configuration -> Administrative Templates -> System -> User Profiles;
  4. Open the option Delete user profiles older than a specified number of days on system restart;
  5. Enable the policy and specify the number of days a user profile is considered active. When this period is over, Windows User Profile Service will automatically delete the profile at the next restart. It is recommended to specify the period of 45-90 days here;Group Policy: Delete user profiles older than a specified number days on system restart
  6. After you apply the new Group Policy settings, User Profile Service on your Windows Server will automatically delete the old user profiles. User profiles will be deleted at the next server reboot.

If you use this policy, you must ensure that there are no problems with the system time when the server is stopped/restarted (check the article “System time and date changes after reboot”). Otherwise, active user profiles may be deleted.

Another disadvantage is that you cannot prevent certain profiles from being removed, such as local accounts, administrators, etc.

This policy didn’t work correctly in versions prior to Windows 11/10 and Windows Server 2022/2019. Previously, user profile inactivity was determined by the date the NTUSER.dat file was modified. When installing Windows updates, the Trusted Installer service can change the modification date of the NTUSER.dat file in each user’s profile. As a result, the Win32_UserProfile service thinks that the profile has been used recently.

In modern versions of Windows, this Group Policy option checks for user profile activity against the values of the LocalProfileUnloadTimeLow and LocalProfileUnloadTimeHigh parameters under in HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NTCurrentVersionProfileList

Get profile load time from registry parameter LocalProfileUnloadTimeHigh

You can use the following script to get the LocalProfileLoadTimeLow and LocalProfileUnloadTimeHigh registry values in normal time format:

$profilelist = Get-ChildItem "HKLM:SOFTWAREMicrosoftWindows NTCurrentVersionProfileList"
foreach ($p in $profilelist) {
    try {
        $objUser = (New-Object System.Security.Principal.SecurityIdentifier($p.PSChildName)).Translate([System.Security.Principal.NTAccount]).value
    } catch {
        $objUser = "[UNKNOWN]"
  }
    Remove-Variable -Force LTH,LTL,UTH,UTL -ErrorAction SilentlyContinue
    $LTH = '{0:X8}' -f (Get-ItemProperty -Path $p.PSPath -Name LocalProfileLoadTimeHigh -ErrorAction SilentlyContinue).LocalProfileLoadTimeHigh
    $LTL = '{0:X8}' -f (Get-ItemProperty -Path $p.PSPath -Name LocalProfileLoadTimeLow -ErrorAction SilentlyContinue).LocalProfileLoadTimeLow
    $UTH = '{0:X8}' -f (Get-ItemProperty -Path $p.PSPath -Name LocalProfileUnloadTimeHigh -ErrorAction SilentlyContinue).LocalProfileUnloadTimeHigh
    $UTL = '{0:X8}' -f (Get-ItemProperty -Path $p.PSPath -Name LocalProfileUnloadTimeLow -ErrorAction SilentlyContinue).LocalProfileUnloadTimeLow
    $LoadTime = if ($LTH -and $LTL) {
        [datetime]::FromFileTime("0x$LTH$LTL")
    } else {
        $null
    }
    $UnloadTime = if ($UTH -and $UTL) {
        [datetime]::FromFileTime("0x$UTH$UTL")
    } else {
        $null
    }
    [pscustomobject][ordered]@{
        User = $objUser
        SID = $p.PSChildName
        Loadtime = $LoadTime
        UnloadTime = $UnloadTime
    }
} 

Get last profile load and unload date with PowerShell

This list contains the last load time for each user profile.

Delete Old User Profiles with PowerShell Script

Instead of using the automatic profile cleanup policy described above, you can use a simple PowerShell script to find and remove the profiles of disabled or inactive users.

First, let’s try to calculate the size of each user’s profile in C:Users using a simple script from the article Getting Folder Size with PowerShell

gci -force ‘C:Users’-ErrorAction SilentlyContinue | Where { !($_.Attributes -match " ReparsePoint") }| ? { $_ -is [io.directoryinfo] } | % {
$len = 0
gci -recurse -force $_.fullname -ErrorAction SilentlyContinue | % { $len += $_.length }
$_.fullname, ‘{0:N2} GB’ -f ($len / 1Gb)
$sum = $sum + $len
}
"Total size of profiles",'{0:N2} GB' -f ($sum / 1Gb)

The total size of all user profiles in C:Users is about 32 GB.

count the total user profile size on RDS host

Let’s see the list of users whose profiles have not been used for more than 60 days. You can use the value in the LastUseTime field of the profile to find them.

Get-WMIObject -class Win32_UserProfile | Where {(!$_.Special) -and ($_.ConvertToDateTime($_.LastUseTime) -lt (Get-Date).AddDays(-60))}| Measure-Object

It turned out that I had 127 inactive user accounts on my RDS host (with profiles total size of about 18 GB).

get inactive users list by profile LastUseTime on RDSH

The following PowerShell script lists the details of user profiles that have not been updated for more than 60 days. The script converts the user’s SID to a name, calculates the size of each user’s profile, and displays a resulting table:

$allprofilesinfo = @()
$OldProfiles=Get-WMIObject -class Win32_UserProfile | Where {(!$_.Special) -and ($_.ConvertToDateTime($_.LastUseTime) -lt (Get-Date).AddDays(-60))}
Foreach ($OldProfile in $OldProfiles)
   {$objSID = New-Object System.Security.Principal.SecurityIdentifier ($OldProfile.SID)
    $objUser = $objSID.Translate( [System.Security.Principal.NTAccount])
        $userinfo = New-Object PSObject -Property @{
            userName = $objUser.Value
            ProfilePath = $OldProfile.localpath
            LastUsedDate = $OldProfile.ConvertToDateTime($OldProfile.LastUseTime)
            FolderSize =  "{0:N2} GB" -f ((gci –force $OldProfile.localpath –Recurse -ErrorAction SilentlyContinue| measure Length -s).sum / 1Gb) 
        }
    $allprofilesinfo += $userinfo
   }
$allprofilesinfo

powershell: list local profiles info

To remove all these user profiles, it is sufficient to pipe the list of users to the Remove-WmiObject command (it is recommended that you check the output of the script with the -WhatIf parameter before running it):

Get-WMIObject -class Win32_UserProfile | Where {(!$_.Special) -and (!$_.Loaded) -and ($_.ConvertToDateTime($_.LastUseTime) -lt (Get-Date).AddDays(-30))} | Remove-WmiObject –WhatIf

As mentioned earlier, when installing some Windows updates, the Trusted Installer service can change the modification date of the NTUSER.dat file in each user’s profile.

The screenshot above shows that all profiles were changed at about the same time. Check the date of the last updates installed in Windows:

gwmi win32_quickfixengineering |sort installedon  |select InstalledOn -Last 1

Or using the PSWindowsUpdate module:

Get-WUHistory | Select-Object -First 10

It will most likely coincide with the date the profiles were changed. Therefore, on earlier versions of Windows, you can get a list of inactive profiles using another script that checks the lastwritetime attribute of the user’s profile directory:

$USERS= (Get-ChildItem -directory -force 'C:Users' | Where { ((Get-Date) — $_.lastwritetime).days -ge 60 } | % {'c:users' + $_.Name})
foreach ($User in $USERS) {
Get-WMIObject -class Win32_UserProfile | Where {(!$_.Special) -and (!$_.Loaded) -and ($_.LocalPath -eq $User)} | Remove-WmiObject WhatIf }

To avoid deleting the profiles of some users (such as System and Network Service accounts, a local administrator account, accounts of users having active sessions, and other accounts from the exception list), you can modify the script as follows:

#The list of accounts, which profiles must not be deleted
$ExcludedUsers ="Public","zabbix_agent","svc",”user_1”,”user_2”
$LocalProfiles=Get-WMIObject -class Win32_UserProfile | Where {(!$_.Special) -and (!$_.Loaded) -and ($_.ConvertToDateTime($_.LastUseTime) -lt (Get-Date).AddDays(-60))}
foreach ($LocalProfile in $LocalProfiles)
{
if (!($ExcludedUsers -like $LocalProfile.LocalPath.Replace("C:Users","")))
{
$LocalProfile | Remove-WmiObject
Write-host $LocalProfile.LocalPath, "profile deleted” -ForegroundColor Magenta
}
}

You can run this PowerShell script via a GPO at shutdown or with a PowerShell script in Task Scheduler.

It is recommended that you test the script in your environment before configuring automatic profile deletion!

You can modify the script to automatically delete all user profiles added to the specific AD group. For example, you want to delete the profiles of users who have quit. Just add these accounts to the DisabledUsers group and run the script on the target host:

$users = Get-ADGroupMember -Identity DisabledUsers | Foreach {$_.Sid.Value}
$profiles = Get-WmiObject Win32_UserProfile
$profiles | Where {$users -eq $_.Sid} | Foreach {$_.Delete()}

Share this post

Leave a Reply

Your email address will not be published. Required fields are marked *