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

SCRIPT LIBRARY · POWERSHELL

Close Everything Before a Game, Without Killing Windows

Politely close your open apps (and, if you ask, tray apps and named services) to free up memory before a game or a long render, while Windows, your security software and the terminal you're typing in stay put.

AT A GLANCEStop-NonEssentialProcess.ps1
What it does
Finds the apps running in your session, asks each one to close the way clicking X would, waits a few seconds, and reports what closed. Background apps and services are only included when you ask. Windows processes, other users' processes, services and anything on your -Exclude list are never touched.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • No modules
Permissions
None for your own apps. Stopping services with -StopService needs an elevated PowerShell.
Runs on
Windows 10/11
Tested
Parse-checked, then run in PowerShell 7.4 on Linux against dummy background processes with -WhatIf and for real (window-closing and service stopping are Windows-only and weren't exercised there); confirmed it skips its own parent shell and processes in other sessions

Part 10 of the thread Spring cleaning for Windows PCs

Before a big game, a long video export or a VM that wants every gigabyte, it's nice to start clean: no browser with forty tabs, no chat app, no three launchers all updating themselves. Closing them one at a time is tedious, so for years I had a pair of batch files for it. "Original Flavor" ran taskkill /f on everything I owned except Explorer and cmd. "Flaming Hot" did that too, then stopped about forty services, deleted Prefetch and emptied the temp folders for good measure.

They worked, in the sense that things closed. They also force-killed apps with unsaved work, took out bits of Windows that happened to be running as me, and stopped services (Themes, Windows Search, the diagnostics service) that Windows needs back almost immediately. None of that makes a game run faster.

This version is the polite one. It asks each windowed app to close the way clicking X does, so anything with unsaved work gets to ask. It never touches Windows' own processes, other sessions or services unless you name them. -WhatIf shows the list first, and every close asks for confirmation unless you tell it not to.

Stop-NonEssentialProcess.ps1Download
<#
.SYNOPSIS
    Closes your open apps to free up memory and CPU before a game or a big job, without touching Windows itself.
.DESCRIPTION
    Finds processes running in your own session and asks each windowed app to close, the same as
    clicking its X, so it can save or prompt you. Anything still open after -GracePeriodSeconds is
    left alone unless you add -Force, which ends it.

    Never touched: processes in other sessions (services run in session 0), anything that runs from
    the Windows folder, a built-in list of shell and security processes, this PowerShell window and
    the terminal it runs in, and anything matching -Exclude. Background (windowless) apps such as tray
    icons are only included with -IncludeBackground. Services are only stopped if you name them with
    -StopService.

    Every close asks for confirmation (answer A for Yes to All), and -WhatIf lists what would close
    without closing anything. Returns one object per process or service.
.PARAMETER Exclude
    Process names to keep running, wildcards allowed (for example steam*, discord, obs64).
.PARAMETER IncludeBackground
    Also end windowless processes from outside the Windows folder, such as tray apps and updaters.
    These have no window to close politely, so they're ended directly.
.PARAMETER GracePeriodSeconds
    How long to wait for apps to close on their own. Default: 10.
.PARAMETER Force
    End any app that's still running after the grace period (usually one showing a "save changes?" prompt).
.PARAMETER StopService
    Service names to stop as well. Nothing is stopped unless you list it here.
.EXAMPLE
    .\Stop-NonEssentialProcess.ps1 -WhatIf
.EXAMPLE
    .\Stop-NonEssentialProcess.ps1 -Exclude steam*, discord, obs64 -IncludeBackground -Confirm:$false
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
    [string[]]$Exclude = @(),

    [switch]$IncludeBackground,

    [ValidateRange(0, 300)]
    [int]$GracePeriodSeconds = 10,

    [switch]$Force,

    [string[]]$StopService = @()
)

# Shell, input, audio, security and session plumbing. Most live in the Windows folder anyway; this catches the ones that don't.
$protected = @(
    'System', 'Idle', 'Registry', 'smss', 'csrss', 'wininit', 'winlogon', 'services', 'lsass', 'LsaIso', 'svchost', 'fontdrvhost', 'dwm', 'Memory Compression',
    'explorer', 'sihost', 'ctfmon', 'taskhostw', 'RuntimeBroker', 'dllhost', 'conhost', 'audiodg', 'ShellExperienceHost', 'StartMenuExperienceHost', 'SearchHost', 'SearchApp',
    'TextInputHost', 'ApplicationFrameHost', 'LockApp', 'SystemSettings', 'Widgets', 'WidgetService', 'PhoneExperienceHost', 'smartscreen', 'SecurityHealthSystray', 'SecurityHealthService',
    'MsMpEng', 'NisSrv', 'MpDefenderCoreService', 'Taskmgr', 'WindowsTerminal', 'OpenConsole', 'powershell', 'pwsh', 'powershell_ise', 'cmd'
)

$mySession = (Get-Process -Id $PID).SessionId
$windowsDir = if ($env:SystemRoot) { $env:SystemRoot.TrimEnd('\') + '\' } else { $null }

# Keep this script's own process and everything above it (the console, the terminal, the editor that launched it).
$keepIds = [System.Collections.Generic.HashSet[int]]::new()
$parentOf = @{}
try { Get-CimInstance -ClassName Win32_Process -Property ProcessId, ParentProcessId -ErrorAction Stop | ForEach-Object { $parentOf[[int]$_.ProcessId] = [int]$_.ParentProcessId } }
catch { Write-Verbose "Couldn't read parent processes from CIM: $($_.Exception.Message)" }
$walk = $PID
while ($walk -and $keepIds.Add($walk)) {
    $walk = if ($parentOf.ContainsKey($walk)) { $parentOf[$walk] } else { (Get-Process -Id $walk -ErrorAction SilentlyContinue).Parent.Id }
}

function New-Result($Process, [string]$Action, [string]$Detail) {
    [pscustomobject]@{
        Type     = 'Process'
        Name     = $Process.ProcessName
        Id       = $Process.Id
        Window   = $Process.MainWindowTitle
        MemoryMB = [math]::Round($Process.WorkingSet64 / 1MB, 1)
        Action   = $Action
        Detail   = $Detail
        Path     = $Process.Path
    }
}

$candidates = foreach ($p in Get-Process) {
    if ($p.SessionId -ne $mySession -or $keepIds.Contains($p.Id)) { continue }
    if ($protected -contains $p.ProcessName) { continue }
    if ($Exclude | Where-Object { $p.ProcessName -like $_ }) { Write-Verbose "Excluded: $($p.ProcessName) ($($p.Id))"; continue }

    $path = $null
    try { $path = $p.Path } catch { }
    if (-not $path) { Write-Verbose "Skipped $($p.ProcessName) ($($p.Id)): can't read its path, so it's probably elevated or protected."; continue }
    if ($windowsDir -and $path.StartsWith($windowsDir, [StringComparison]::OrdinalIgnoreCase)) { continue }

    $hasWindow = $p.MainWindowHandle -ne [IntPtr]::Zero
    if (-not $hasWindow -and -not $IncludeBackground) { continue }
    [pscustomobject]@{ Process = $p; HasWindow = $hasWindow }
}

$waiting = [System.Collections.Generic.List[object]]::new()
foreach ($c in $candidates | Sort-Object { $_.Process.ProcessName }) {
    $p = $c.Process
    $label = if ($p.MainWindowTitle) { "$($p.ProcessName) ($($p.Id)) '$($p.MainWindowTitle)'" } else { "$($p.ProcessName) ($($p.Id))" }
    $verb = if ($c.HasWindow) { 'Close app window' } else { 'End background process' }
    if (-not $PSCmdlet.ShouldProcess($label, $verb)) { continue }

    try {
        if ($c.HasWindow) {
            if ($p.CloseMainWindow()) { $waiting.Add($p) }
            else { New-Result $p 'Failed' 'The window refused the close request.' }
        }
        else {
            $result = New-Result $p 'Ended' 'Background process'
            Stop-Process -Id $p.Id -Force -ErrorAction Stop -Confirm:$false
            $result
        }
    }
    catch {
        New-Result $p 'Failed' $_.Exception.Message
    }
}

# Give the polite closes time to finish, then deal with whatever's left.
$deadline = (Get-Date).AddSeconds($GracePeriodSeconds)
while ($waiting.Count -and (Get-Date) -lt $deadline) {
    foreach ($p in @($waiting)) {
        $p.Refresh()
        if ($p.HasExited) { New-Result $p 'Closed' $null; [void]$waiting.Remove($p) }
    }
    if ($waiting.Count) { Start-Sleep -Milliseconds 500 }
}
foreach ($p in $waiting) {
    if ($Force) {
        try { Stop-Process -Id $p.Id -Force -ErrorAction Stop -Confirm:$false; New-Result $p 'Ended' "Still open after $GracePeriodSeconds s" }
        catch { New-Result $p 'Failed' $_.Exception.Message }
    }
    else {
        New-Result $p 'StillRunning' 'Probably waiting on a save prompt. Use -Force to end it.'
    }
}

foreach ($name in $StopService) {
    $svc = Get-Service -Name $name -ErrorAction SilentlyContinue
    if (-not $svc) { [pscustomobject]@{ Type = 'Service'; Name = $name; Id = $null; Window = $null; MemoryMB = $null; Action = 'NotFound'; Detail = $null; Path = $null }; continue }
    if ($svc.Status -ne 'Running') { [pscustomobject]@{ Type = 'Service'; Name = $svc.Name; Id = $null; Window = $svc.DisplayName; MemoryMB = $null; Action = 'AlreadyStopped'; Detail = [string]$svc.Status; Path = $null }; continue }
    if (-not $PSCmdlet.ShouldProcess("$($svc.DisplayName) ($($svc.Name))", 'Stop service')) { continue }
    try {
        Stop-Service -InputObject $svc -ErrorAction Stop -Confirm:$false
        [pscustomobject]@{ Type = 'Service'; Name = $svc.Name; Id = $null; Window = $svc.DisplayName; MemoryMB = $null; Action = 'Stopped'; Detail = 'Starts again on the next reboot if set to Automatic'; Path = $null }
    }
    catch {
        [pscustomobject]@{ Type = 'Service'; Name = $svc.Name; Id = $null; Window = $svc.DisplayName; MemoryMB = $null; Action = 'Failed'; Detail = $_.Exception.Message; Path = $null }
    }
}

Parameters

ParameterTypeDefaultWhat it's for
-Excludestring[]—Process names to leave running, wildcards allowed. Your game launcher, voice chat and capture software go here.
-IncludeBackgroundswitch—Also end windowless processes that aren't part of Windows, like tray apps and updaters. They have no window to close, so they're ended directly.
-GracePeriodSecondsint10How long to give apps to close before reporting (or, with -Force, ending) the stragglers.
-Forceswitch—End apps that are still open after the grace period. Usually that's an app sitting on a "save changes?" prompt, so be sure.
-StopServicestring[]—Service names to stop too. There's no default list on purpose. Needs an elevated PowerShell.

Run it

See what would close, without closing anything.

.\Stop-NonEssentialProcess.ps1 -WhatIf

Game night. Keep the launcher, voice chat and OBS, close the rest, no prompts.

.\Stop-NonEssentialProcess.ps1 -Exclude steam*, discord, obs64 -IncludeBackground -Confirm:$false

Before a long export, close everything and stop the search indexer until the next reboot.

.\Stop-NonEssentialProcess.ps1 -Exclude 'Adobe Premiere Pro' -StopService WSearch -Force

How much memory did that give back?

.\Stop-NonEssentialProcess.ps1 -IncludeBackground -Confirm:$false | Where-Object Action -in 'Closed', 'Ended' | Measure-Object MemoryMB -Sum

What you'll see

Example outputvalues are illustrative
Type    Name          Id Window                          MemoryMB Action       Detail
----    ----          -- ------                          -------- ------       ------
Process OneDrive    9120                                    141.2 Ended        Background process
Process Spotify    11876 Spotify Premium                    312.7 Closed
Process chrome     14208 Budget 2026 - Google Sheets        486.0 Closed
Process msedge     15632 Contoso Intranet - Profile 1       398.4 Closed
Process EXCEL      17004 Q3 numbers.xlsx - Excel            204.9 StillRunning Probably waiting on a save prompt. Use -Force to end it.
Service WSearch                Windows Search                     Stopped      Starts again on the next reboot if set to Automatic

How it works

  1. Work out what's off limits. It notes your session ID, the Windows folder, and this PowerShell process plus every parent above it (the console window, Windows Terminal, VS Code), so it can't close the thing running it. A short built-in list covers the shell and security processes that don't live in the Windows folder.
  2. Build the candidate list. From Get-Process, it keeps only processes in your session that aren't protected, don't match -Exclude, and run from outside the Windows folder. Anything whose path it can't read is skipped, because that usually means it's elevated or protected. Without -IncludeBackground, only apps with a visible window make the list.
  3. Ask, don't kill. Each app goes through ShouldProcess, so -WhatIf and the confirmation prompts work. Windowed apps get CloseMainWindow(), the same message as clicking X. Background processes, which have no window, are ended with Stop-Process.
  4. Wait, then report. It checks every half second for up to -GracePeriodSeconds. Apps that exit are reported as Closed. Anything left is StillRunning, or Ended if you used -Force.
  5. Services last, and only by name. Each service in -StopService is checked and stopped with its own confirmation. Nothing is stopped by default.

Every row comes back as an object with the memory each process was using, so you can total up what you freed. If the drive is the problem rather than memory, the disk cleanup script is the better starting point.

Take it further

  • Make a shortcut per occasion. A desktop shortcut running powershell.exe -File Stop-NonEssentialProcess.ps1 -Exclude steam* -Confirm:$false turns it into a one-click "game mode".
  • Bring things back afterwards. Save the output with Export-Clixml, and a second script can relaunch whatever was closed from each process's path once you're done.
  • Keep a personal exclude list. Put your always-keep names in a text file and pass -Exclude (Get-Content .\keep.txt).

Things that'll trip you up

  • Answer A at the first prompt. With confirmation on, you're asked about every process. Type A (Yes to All) once you've checked the list, or run with -Confirm:$false when you already trust it. -WhatIf first is still a good habit.
  • Browsers close all their windows. Closing a browser's main window usually closes the whole browser, which is the point. If you'd rather keep one open for a guide or a wiki, add it to -Exclude.
  • Some apps hide in the tray instead of closing. Plenty of chat and sync apps treat the X button as "minimize to tray". They'll show up as Closed from the window's point of view but keep running in the background. -IncludeBackground catches them on the next run.
  • Elevated apps are skipped. An app running as administrator can't be read or closed from a normal PowerShell, so it's quietly left alone. Run the script elevated if you need those closed too.
  • Stopping services rarely helps a game. Idle services use almost nothing. Stopping the search indexer before a heavy disk job can help; stopping a list of forty mostly doesn't, and some restart on their own. It's opt-in for that reason.