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.
- 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.
<#
.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
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Exclude | string[] | — | Process names to leave running, wildcards allowed. Your game launcher, voice chat and capture software go here. |
-IncludeBackground | switch | — | 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. |
-GracePeriodSeconds | int | 10 | How long to give apps to close before reporting (or, with -Force, ending) the stragglers. |
-Force | switch | — | End apps that are still open after the grace period. Usually that's an app sitting on a "save changes?" prompt, so be sure. |
-StopService | string[] | — | 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 -WhatIfGame night. Keep the launcher, voice chat and OBS, close the rest, no prompts.
.\Stop-NonEssentialProcess.ps1 -Exclude steam*, discord, obs64 -IncludeBackground -Confirm:$falseBefore a long export, close everything and stop the search indexer until the next reboot.
.\Stop-NonEssentialProcess.ps1 -Exclude 'Adobe Premiere Pro' -StopService WSearch -ForceHow much memory did that give back?
.\Stop-NonEssentialProcess.ps1 -IncludeBackground -Confirm:$false | Where-Object Action -in 'Closed', 'Ended' | Measure-Object MemoryMB -SumWhat you'll see
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
- 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.
- 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. - Ask, don't kill. Each app goes through
ShouldProcess, so -WhatIf and the confirmation prompts work. Windowed apps getCloseMainWindow(), the same message as clicking X. Background processes, which have no window, are ended withStop-Process. - 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. - Services last, and only by name. Each service in
-StopServiceis 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:$falseturns 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.