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

SCRIPT LIBRARY · POWERSHELL

Clear Browser Caches for Every Profile, Without Touching Cookies

Empty the disk caches for Chrome, Edge, Brave, Opera and Firefox across every browser profile, and see how much space each one gave back, while cookies, history and passwords stay put.

AT A GLANCEClear-BrowserCache.ps1
What it does
Finds every Chrome, Edge, Brave, Opera and Firefox profile under a Windows user profile, empties only the disk cache folders (Cache, Code Cache, GPUCache and Service Worker CacheStorage for the Chromium browsers, cache2 for Firefox), skips or closes browsers that are running, and returns the size and space freed per browser and profile.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • No modules
Permissions
A normal user can clean their own profile. Cleaning other users' profiles under C:\Users needs an elevated session.
Runs on
Windows 10/11
Tested
Parse-checked and run in PowerShell 7.4 against a temp folder laid out like a Windows user profile (two Chrome profiles, Edge, Brave under a second user, Opera split across Local and Roaming, two Firefox profiles), with -WhatIf and for real, confirming cookies, history, login data, bookmarks, Local Storage and the Service Worker database were left alone. The running-browser skip, the declined prompt and -CloseBrowser -Force were tested with a stand-in process; real browsers weren't, since this ran on Linux.

Part 12 of the thread Spring cleaning for Windows PCs

Browser caches are the junk drawer of a Windows profile. Every site you've visited leaves a few megabytes of scripts, images and compiled shaders behind, and with two or three browsers and a couple of profiles each, it's easy to find several gigabytes sitting in AppData doing nothing useful.

The usual fix is the browser's own "clear browsing data" dialog, which is one profile at a time, one browser at a time, and one careless tick away from logging you out of everything. The all-in-one cleanup tools are worse; half of them treat cookies and history as fair game.

This started as the browser section of a much bigger cleanup script I ran on my own PCs, which only knew about each browser's Default profile. Pulled out on its own, it finds every profile, clears only the cache folders, and tells you what it got back, profile by profile.

Clear-BrowserCache.ps1Download
<#
.SYNOPSIS
    Clears the disk caches for Chrome, Edge, Brave, Opera and Firefox, profile by profile, and reports the space freed.
.DESCRIPTION
    Finds every browser profile under a Windows user profile (Default, Profile 1, Profile 2 and so on for the
    Chromium browsers, every profile folder for Firefox) and empties the cache folders only:

      Chrome, Edge, Brave, Opera   Cache, Code Cache, GPUCache, Service Worker\CacheStorage
      Firefox                      cache2

    Nothing else is touched. Cookies, history, saved passwords, bookmarks, Local Storage, IndexedDB, extensions and
    settings all live in other files and folders, and the script never goes near them.

    A browser that's running is skipped, because it holds its cache files open. Add -CloseBrowser to close it first
    (you'll be asked to confirm unless you add -Force). Returns one object per browser profile with the size before
    and the space freed. Supports -WhatIf.
.PARAMETER Browser
    Which browsers to clean. Default: all five.
.PARAMETER UserProfile
    One or more Windows user profile folders to clean. Default: the current user. Pass the folders under C:\Users
    (from an elevated session) to do every user on the machine.
.PARAMETER CloseBrowser
    Close a running browser before cleaning it, instead of skipping it.
.PARAMETER Force
    With -CloseBrowser, don't ask before closing.
.EXAMPLE
    .\Clear-BrowserCache.ps1 -WhatIf
.EXAMPLE
    .\Clear-BrowserCache.ps1 -Browser Chrome, Edge -CloseBrowser
.EXAMPLE
    Get-ChildItem C:\Users -Directory | .\Clear-BrowserCache.ps1 -Browser Firefox
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidateSet('Chrome', 'Edge', 'Brave', 'Opera', 'Firefox')]
    [string[]]$Browser = @('Chrome', 'Edge', 'Brave', 'Opera', 'Firefox'),

    [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('FullName')]
    [string[]]$UserProfile = $env:USERPROFILE,

    [switch]$CloseBrowser,
    [switch]$Force
)

begin {
    # Where each browser keeps its profiles (relative to the user profile), and the process name to check for.
    $browsers = [ordered]@{
        Chrome  = @{ Process = 'chrome';  Kind = 'Chromium'; Roots = @('AppData\Local\Google\Chrome\User Data') }
        Edge    = @{ Process = 'msedge';  Kind = 'Chromium'; Roots = @('AppData\Local\Microsoft\Edge\User Data') }
        Brave   = @{ Process = 'brave';   Kind = 'Chromium'; Roots = @('AppData\Local\BraveSoftware\Brave-Browser\User Data') }
        # Opera keeps the profile itself in Roaming and the cache in Local, with no Default folder.
        Opera   = @{ Process = 'opera';   Kind = 'Opera';    Roots = @('AppData\Local\Opera Software\Opera Stable', 'AppData\Roaming\Opera Software\Opera Stable', 'AppData\Local\Opera Software\Opera GX Stable', 'AppData\Roaming\Opera Software\Opera GX Stable') }
        # Firefox's cache2 lives in Local, not in the Roaming profile folder next to your bookmarks.
        Firefox = @{ Process = 'firefox'; Kind = 'Firefox';  Roots = @('AppData\Local\Mozilla\Firefox\Profiles') }
    }
    $chromiumCaches = @('Cache', 'Code Cache', 'GPUCache', 'Service Worker\CacheStorage')

    function Get-FolderSize([string]$Folder) {
        if (-not (Test-Path -LiteralPath $Folder)) { return [long]0 }
        $sum = (Get-ChildItem -LiteralPath $Folder -File -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
        [long]$(if ($sum) { $sum } else { 0 })
    }

    # Returns @{ ProfileName = @(cache folders) } for one browser under one user profile.
    function Get-CacheTarget([string]$UserHome, $Info) {
        $targets = [ordered]@{}
        foreach ($rel in $Info.Roots) {
            $root = Join-Path $UserHome $rel
            if (-not (Test-Path -LiteralPath $root -PathType Container)) { continue }
            switch ($Info.Kind) {
                'Chromium' {
                    $profiles = Get-ChildItem -LiteralPath $root -Directory | Where-Object { $_.Name -eq 'Default' -or $_.Name -like 'Profile *' -or $_.Name -eq 'Guest Profile' }
                    foreach ($p in $profiles) { $targets[$p.Name] = @($chromiumCaches | ForEach-Object { Join-Path $p.FullName $_ }) }
                }
                'Opera' {
                    $name = Split-Path $root -Leaf
                    $list = @(if ($targets.Contains($name)) { $targets[$name] })
                    $targets[$name] = $list + @($chromiumCaches | ForEach-Object { Join-Path $root $_ })
                }
                'Firefox' {
                    foreach ($p in Get-ChildItem -LiteralPath $root -Directory) { $targets[$p.Name] = @(Join-Path $p.FullName 'cache2') }
                }
            }
        }
        $targets
    }

    $userList = [System.Collections.Generic.List[string]]::new()
}

process {
    foreach ($u in $UserProfile) { if ($u) { $userList.Add($u) } }
}

end {
    foreach ($name in $Browser) {
        $info = $browsers[$name]

        # Is it running? A running browser holds its cache open, so either close it or skip it.
        $running = @(Get-Process -Name $info.Process -ErrorAction SilentlyContinue)
        $skipReason = $null
        if ($running.Count) {
            if (-not $CloseBrowser) {
                $skipReason = 'Skipped: browser is running (use -CloseBrowser)'
            }
            elseif ($WhatIfPreference) {
                Write-Host "What if: would close $name ($($running.Count) process(es)) before cleaning."
            }
            else {
                # Ask before closing. If the prompt can't be shown (a non-interactive session), treat it as a no.
                $ok = $Force
                if (-not $ok) {
                    try { $ok = $PSCmdlet.ShouldContinue("Close every $name window? Tabs usually come back on restart, but anything unsaved in a page will be lost.", "Close $name") }
                    catch { Write-Warning "Couldn't ask about closing $name ($($_.Exception.Message)). Use -Force to close it without asking."; $ok = $false }
                }
                if (-not $ok) { $skipReason = 'Skipped: you chose not to close it' }
            }
            if ($running.Count -and -not $skipReason -and -not $WhatIfPreference) {
                if ($PSCmdlet.ShouldProcess("$name ($($running.Count) process(es))", 'Close browser')) {
                    $running | ForEach-Object { try { [void]$_.CloseMainWindow() } catch { } }
                    $running | Wait-Process -Timeout 10 -ErrorAction SilentlyContinue
                    # Anything still open after 10 seconds (a stuck tab, a background process) gets stopped.
                    $running | Where-Object { -not $_.HasExited } | Stop-Process -Force -ErrorAction SilentlyContinue
                    $running | Wait-Process -Timeout 5 -ErrorAction SilentlyContinue
                    if ($running | Where-Object { -not $_.HasExited }) { $skipReason = 'Skipped: browser would not close' }
                }
                else { $skipReason = 'Skipped: browser is running' }
            }
        }

        foreach ($userHome in $userList) {
            $targets = Get-CacheTarget $userHome $info
            if (-not $targets.Count) { Write-Verbose "$name : no profiles under $userHome"; continue }

            foreach ($profileName in $targets.Keys) {
                $folders = @($targets[$profileName] | Where-Object { Test-Path -LiteralPath $_ -PathType Container })
                $before = [long]0
                foreach ($f in $folders) { $before += Get-FolderSize $f }
                $status = 'Cleared'; $failed = 0

                if ($skipReason) { $status = $skipReason }
                elseif (-not $folders.Count) { $status = 'Nothing to clear' }
                else {
                    foreach ($f in $folders) {
                        if (-not $PSCmdlet.ShouldProcess($f, "Empty $name cache")) { $status = 'WhatIf'; continue }
                        # Empty the folder but keep it, so the browser finds what it expects on the next start.
                        foreach ($item in Get-ChildItem -LiteralPath $f -Force -ErrorAction SilentlyContinue) {
                            try { Remove-Item -LiteralPath $item.FullName -Recurse -Force -ErrorAction Stop }
                            catch { $failed++; Write-Verbose "$($item.FullName): $($_.Exception.Message)" }
                        }
                    }
                    if ($failed) { $status = "Partial: $failed item(s) in use" }
                }

                $after = [long]0
                if ($status -notlike 'Skipped*' -and $status -ne 'WhatIf') { foreach ($f in $folders) { $after += Get-FolderSize $f } } else { $after = $before }

                [pscustomobject]@{
                    Browser     = $name
                    Profile     = $profileName
                    User        = Split-Path $userHome -Leaf
                    Status      = $status
                    SizeMB      = [math]::Round($before / 1MB, 1)
                    FreedMB     = [math]::Round(($before - $after) / 1MB, 1)
                    FreedBytes  = $before - $after
                }
            }
        }
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Browserstring[]Chrome, Edge, Brave, Opera, FirefoxWhich browsers to clean.
-UserProfilestring[]$env:USERPROFILEWindows user profile folders to clean. Takes pipeline input, so Get-ChildItem C:\Users -Directory works from an elevated session.
-CloseBrowserswitch—Close a running browser instead of skipping it. Asks first.
-Forceswitch—With -CloseBrowser, close without asking.

Run it

See what's there and what would be cleared, without touching anything.

.\Clear-BrowserCache.ps1 -WhatIf

Clear Chrome and Edge, closing them first if they're open.

.\Clear-BrowserCache.ps1 -Browser Chrome, Edge -CloseBrowser

Every user on the machine, from an elevated session, with a total at the end.

Get-ChildItem C:\Users -Directory | .\Clear-BrowserCache.ps1 | Tee-Object -Variable r; '{0:N0} MB freed' -f (($r | Measure-Object FreedBytes -Sum).Sum / 1MB)

Just Firefox, and only show the profiles that had something in them.

.\Clear-BrowserCache.ps1 -Browser Firefox | Where-Object SizeMB -gt 0

What you'll see

Example outputvalues are illustrative
Browser Profile                  User  Status                                          SizeMB FreedMB FreedBytes
------- -------                  ----  ------                                          ------ ------- ----------
Chrome  Default                  alex  Cleared                                          842.6   839.1  879872410
Chrome  Profile 1                alex  Cleared                                          311.2   311.2  326316236
Edge    Default                  alex  Partial: 3 item(s) in use                        264.0   251.7  263926579
Brave   Default                  alex  Nothing to clear                                   0.0     0.0          0
Opera   Opera Stable             alex  Cleared                                          128.4   128.4  134637158
Firefox k2x9m4qd.default-release alex  Skipped: browser is running (use -CloseBrowser)   402.9     0.0          0

How it works

  1. Check what's running. For each browser it looks for the process (chrome, msedge, brave, opera, firefox). A running browser holds its cache files open, so it's skipped, unless you add -CloseBrowser. Then it asks, closes the windows politely, waits ten seconds, and stops whatever's left. If it can't ask (a non-interactive session), it treats that as a no.
  2. Find every profile. Chromium browsers keep profiles as Default, Profile 1, Profile 2 and so on under User Data; the script picks up all of them, plus the Guest profile. Firefox gets every folder under Local\Mozilla\Firefox\Profiles. Opera's Local and Roaming folders are checked together.
  3. Measure, empty, measure again. Each cache folder is sized, then its contents are removed item by item, with the folder itself left in place. A file that's locked gets counted and skipped instead of stopping the run. The folder is sized again afterwards, so FreedMB is what actually went, not what it hoped to remove.
  4. Report per profile. You get one object per browser profile with the user, the status, the size before and the space freed. -WhatIf lists every folder it would empty and shows the sizes without deleting anything.

Take it further

  • Fold it into a bigger cleanup. Run it before Invoke-DiskCleanup and you've covered the two places most wasted space hides on a typical PC.
  • Show profile names. Chromium keeps the friendly names (the "Work" and "Personal" you picked) in the Local State file next to the profile folders. Read profile.info_cache from that JSON to swap Profile 1 for the name people actually recognise.
  • Schedule it quietly. A monthly scheduled task running as the user, without -CloseBrowser, clears whatever's closed and skips the rest. No surprises, no lost tabs.

Things that'll trip you up

  • It really does leave your logins alone. Cookies, history, saved passwords, bookmarks, Local Storage and IndexedDB are separate files and folders, and the script only ever opens the four cache folders (or cache2 for Firefox). Service Worker has its own database next to CacheStorage; that stays too, so installed web apps keep working.
  • The cache comes back. That's the point of a cache. The first visit to each site afterwards is a little slower while it refills, and a busy profile will be back to a few hundred megabytes within a week or two. This is for reclaiming space and fixing a stale or corrupt cache, not a diet.
  • Edge in the background counts as running. Edge, and Chrome with "Continue running background apps" on, keep processes alive after the last window closes. The script sees those and skips the browser. -CloseBrowser deals with them, or turn the background setting off.
  • Firefox's cache isn't where its profile is. Firefox keeps bookmarks and settings in Roaming but cache2 in Local, under a folder with the same profile name. That's where the script looks. Opera splits its data the same way, with no Default folder, so it shows up as a single "Opera Stable" (or "Opera GX Stable") profile.