Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Cleaning Up Abandoned Outlook OST Files

Find the old Outlook .ost files nobody is using any more and reclaim the space, without touching the ones Outlook has open.

AT A GLANCERemove-StaleOstFile.ps1
What it does
Finds .ost files in the Outlook data folder that haven't been written to in a set number of days and deletes them, skipping any file that's in use. Works on the current user or every profile on the machine.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • No modules
Permissions
None for your own profile. Local admin (elevated) for -AllProfiles.
Runs on
Windows 10/11, Windows Server 2016+ (including RDS hosts)
Tested
Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4

Part 3 of the thread Spring cleaning for Windows PCs

OST files are Outlook's local copy of a mailbox, and they're big. Ten, twenty, fifty gigabytes isn't unusual. That's fine for the mailbox someone uses every day. It's less fine for the one they set up two years ago for a shared inbox, removed from their profile, and forgot about, because Outlook doesn't clean up after itself. The file just sits there.

Since an OST is only a cache of what's on the server, deleting a stale one is safe: if Outlook ever needs it again, it rebuilds it. The catch is doing it without yanking a file out from under a running Outlook, and without trusting a timestamp Windows may not even be updating.

The original version of this used LastAccessTime and deleted anything it found, no questions asked. This one uses LastWriteTime, checks each file is actually free before touching it, reports what it did as objects, and supports -WhatIf. Start with that.

Remove-StaleOstFile.ps1Download
<#
.SYNOPSIS
    Finds and removes Outlook .ost files that haven't been written to in a while.
.DESCRIPTION
    Looks in the Outlook data folder for the current user, or every local profile with
    -AllProfiles, and removes .ost files whose last write is older than -Days. Any file
    that's open (Outlook running, a search indexer holding it) is skipped rather than
    fought with. Returns one object per file, so you can review before you commit.
    Supports -WhatIf, and you should use it first.
.PARAMETER Days
    Minimum age, in days since the file was last written. Default: 60.
.PARAMETER AllProfiles
    Check every local user profile, not just yours. Needs to run elevated.
.PARAMETER MinimumSizeMB
    Ignore files smaller than this. Handy when you only care about the big ones. Default: 0.
.EXAMPLE
    .\Remove-StaleOstFile.ps1 -WhatIf
.EXAMPLE
    .\Remove-StaleOstFile.ps1 -AllProfiles -Days 90 -MinimumSizeMB 500
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidateRange(7, 3650)]
    [int]$Days = 60,

    [switch]$AllProfiles,

    [ValidateRange(0, 1048576)]
    [int]$MinimumSizeMB = 0
)

$cutoff = (Get-Date).AddDays(-$Days)

$outlookFolders = if ($AllProfiles) {
    Get-CimInstance -ClassName Win32_UserProfile -Filter 'Special = FALSE' |
        ForEach-Object { Join-Path $_.LocalPath 'AppData\Local\Microsoft\Outlook' }
}
else {
    Join-Path $env:LOCALAPPDATA 'Microsoft\Outlook'
}

foreach ($folder in $outlookFolders) {
    if (-not (Test-Path -LiteralPath $folder)) { continue }

    # LastWriteTime, not LastAccessTime. Access times are often not updated on modern
    # Windows, and an antivirus scan can bump them anyway.
    $candidates = Get-ChildItem -LiteralPath $folder -Filter '*.ost' -File -Force -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt $cutoff -and $_.Length -ge ($MinimumSizeMB * 1MB) }

    foreach ($file in $candidates) {
        $row = [pscustomobject]@{
            Path          = $file.FullName
            SizeMB        = [math]::Round($file.Length / 1MB, 1)
            LastWriteTime = $file.LastWriteTime
            Action        = $null
        }

        # If we can't get an exclusive handle, something is using it. Leave it be.
        try {
            $stream = [System.IO.File]::Open($file.FullName, 'Open', 'ReadWrite', 'None')
            $stream.Dispose()
        }
        catch {
            $row.Action = 'SkippedInUse'
            $row
            continue
        }

        if ($PSCmdlet.ShouldProcess($file.FullName, "Delete OST ($($row.SizeMB) MB, last written $($file.LastWriteTime.ToString('yyyy-MM-dd')))")) {
            try {
                Remove-Item -LiteralPath $file.FullName -Force -Confirm:$false -ErrorAction Stop
                $row.Action = 'Removed'
            }
            catch {
                $row.Action = "Failed: $($_.Exception.Message)"
            }
        }
        else {
            $row.Action = if ($WhatIfPreference) { 'WouldRemove' } else { 'Skipped' }
        }
        $row
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Daysint60A file has to go this many days without being written to before it's a candidate. The minimum is 7, on purpose.
-AllProfilesswitch—Check every local user profile, not just the one running the script. Needs an elevated session.
-MinimumSizeMBint0Ignore anything smaller than this, if you only care about the big ones.

Run it

See what would go from your own profile.

.\Remove-StaleOstFile.ps1 -WhatIf

Every profile on a shared machine, only files over 500 MB that are 90+ days stale.

.\Remove-StaleOstFile.ps1 -AllProfiles -Days 90 -MinimumSizeMB 500 -WhatIf

Do it, then total up the space.

.\Remove-StaleOstFile.ps1 -AllProfiles | Where-Object Action -eq 'Removed' | Measure-Object SizeMB -Sum

Save a record of what happened.

.\Remove-StaleOstFile.ps1 -AllProfiles | Export-Csv .\ost-cleanup.csv -NoTypeInformation

What you'll see

Example outputvalues are illustrative
Path                                                                        SizeMB LastWriteTime        Action
----                                                                        ------ -------------        ------
C:\Users\adele.v\AppData\Local\Microsoft\Outlook\[email protected] 8841.3 3/14/2026 9:02:11 AM Removed
C:\Users\megan.b\AppData\Local\Microsoft\Outlook\[email protected]     15320.9 6/2/2026 4:47:30 PM  SkippedInUse
C:\Users\alex.w\AppData\Local\Microsoft\Outlook\[email protected]        6102.5 5/20/2026 8:15:02 AM Removed

How it works

  1. Find the folders. For the current user that's %LOCALAPPDATA%\Microsoft\Outlook. With -AllProfiles it asks Win32_UserProfile for every non-system profile and checks the same folder in each one.
  2. Filter by age and size. Only .ost files last written before the cutoff (and above -MinimumSizeMB, if you set it) make the list.
  3. Make sure nobody's using it. Before deleting, the script tries to open the file with an exclusive lock. If Outlook, the search indexer, or anything else has it open, that fails, and the file is reported as SkippedInUse and left alone.
  4. Delete, or pretend to. Under -WhatIf each file comes back as WouldRemove. Otherwise it's deleted and marked Removed, or Failed with the reason.

Take it further

  • Shrink them instead. If the files are big because they're in use, the fix is the Cached Exchange Mode sync window ("download email for the past…"), which you can set by Group Policy or Intune. A three-month window instead of "All" makes a huge difference.
  • Look at RDS and shared PCs first. That's where you'll find OSTs from people who left a year ago. Better yet, clean up the whole profile with the user profile cleanup script and the OST goes with it.
  • Schedule it. A monthly task running as SYSTEM with -AllProfiles -Days 90 keeps shared machines from filling up.

Things that'll trip you up

  • "This computer only" folders live in the OST. For Exchange and Microsoft 365 mailboxes, everything is on the server. For IMAP accounts in Outlook 2013 and later, folders marked "This computer only" exist only in the local OST. Check before you delete one of those.
  • Deleting an active OST means a full resync. If the account is still in use, Outlook will rebuild the file on next launch and download the mailbox again. On a slow link or a 50 GB mailbox, that's a bad morning. The age threshold is there so you only catch files nobody's opened in months.
  • LastAccessTime lies. Windows often doesn't update last-access times, and an antivirus scan can bump them anyway. The script uses LastWriteTime, which Outlook updates constantly while a file is in use.
  • New Outlook doesn't use OST files. The new Outlook for Windows keeps its cache elsewhere, so this only applies to classic Outlook. On a machine where everyone has moved over, the old OSTs are all fair game.