Have you seen the price of RAM these days ...
Summary: Andrew Callaghan discusses the strategy of starting 30 PowerShell instances on a Windows collector, which can cause excessive memory caching. They question why the setup doesn't allow the number of instances to start small and grow organically to the preset maximum instead, suggesting a more efficient approach to managing resources. They clarify that no additional modules are being imported into the sessions.
Hi All
In a large or x-large windows collector the default is to start up 30 PowerShell instances from the get go, not peak at , start at. LM sets a max limit off 100 sessions.
PowerShell caches to memory and that's fine, but since scripts can generally land on any instance, the cache in each session is starting to get a bit out of hand. As per the below.

Since we spec a minimum count, for ex. powershell.spse.process.count.min=30 , why isn't this set to something like 2 all over, and then let it organically grow to the predefined max we already have powershell.spse.process.count.max=100 ? . I've set some of our L and XL collectors down to 5 or 10 PowerShell threads as a minimum and, as at writing, not one of them has grown past the base minimum. On each there are approx 300 wintel servers a smattering of SQL and a couple of VCentre servers. I've been setting lower process minimums on others, on nano its 1 and small its 2.
At this time I'm not seeing any compelling reason to let all these PowerShell sessions run. Before anybody asks, no, there's no extra 'import-modules' in these sessions, its all cache, no there's no queues in WMI, script or batchscript, you can examine what's going on with this script and flush the cache for the largest PID if you want to play around. Drop this into ISE.
# 1. Enumerate all running powershell processes, excluding the current ISE session ($PID)
$AllPipedProcesses = Get-Process -Name "powershell" | Where-Object { $_.Id -ne $PID }
if (-not $AllPipedProcesses) {
Write-Warning "No other running PowerShell processes found to investigate."
return
}
# 2. MODIFIED: Automatically select the PID with the LARGEST Private Memory footprint
$TargetProcess = $AllPipedProcesses | Sort-Object PrivateMemorySize64 -Descending | Select-Object -First 1
$TargetPID = $TargetProcess.Id
Write-Host "Found $($AllPipedProcesses.Count) active PowerShell processes." -ForegroundColor Gray
Write-Host "Targeting LARGEST resource consumer: PID $TargetPID" -ForegroundColor Cyan
Write-Host "Interrogating local PID $TargetPID via ISE..." -ForegroundColor Gray
# 3. Calculate Process Uptime
$StartTime = $TargetProcess.StartTime
$Uptime = (Get-Date) - $StartTime
$UptimeString = "{0} Days, {1} Hours, {2} Minutes" -f $Uptime.Days, $Uptime.Hours, $Uptime.Minutes
# 4. Collect Task Manager Metrics (Before Optimization)
$PrivateWS_Before = [Math]::Round($TargetProcess.PrivateMemorySize64 / 1MB, 2)
$TotalWS_Before = [Math]::Round($TargetProcess.WorkingSet64 / 1MB, 2)
$PeakWS = [Math]::Round($TargetProcess.PeakWorkingSet64 / 1MB, 2)
# 5. Interrogate Loaded Modules (DLLs/Binaries)
$LoadedModules = $TargetProcess.Modules | Select-Object ModuleName, @{Name="Size(MB)"; Expression={[Math]::Round($_.ModuleMemorySize / 1MB, 2)}}
# 6. Calculate Estimate Reclaimable RAM based on baseline unmapped memory
$BaselineProcessSize = 30.0
$EstFreedMB = [Math]::Max(0, [Math]::Round($PrivateWS_Before - $BaselineProcessSize, 2))
# --- OUTPUT REPORT ---
Write-Host "`n--- OS Memory Allocation (Task Manager Fields) ---" -ForegroundColor Yellow
[PSCustomObject]@{
"PID" = $TargetProcess.Id
"Process Name" = $TargetProcess.ProcessName
"Process Uptime" = $UptimeString
"Memory (Private Working Set)" = "$PrivateWS_Before MB"
"Working Set (Memory)" = "$TotalWS_Before MB"
"Peak Working Set" = "$PeakWS MB"
"Commit Size" = "$PrivateWS_Before MB"
} | Format-List
Write-Host "--- Internal Session Analysis ---" -ForegroundColor Yellow
[PSCustomObject]@{
"Estimated Reclaimable RAM" = "$EstFreedMB MB"
"Total Loaded Binaries/DLLs" = $LoadedModules.Count
} | Format-List
Write-Host "--- Top 10 Heaviest Loaded Modules/DLLs in Memory ---" -ForegroundColor Yellow
$LoadedModules | Sort-Object "Size(MB)" -Descending | Select-Object -First 10 | Format-Table -AutoSize
# --- INTERACTIVE ACTION PROMPT ---
Write-Host ""
$Choice = Read-Host "Would you like to force a Garbage Collection on PID $TargetPID now? (Y/N)"
if ($Choice -match "^[Yy]$") {
Write-Host "Executing internal Garbage Collection loop on PID $TargetPID..." -ForegroundColor Cyan
$CleanupBlock = {
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
[System.GC]::Collect()
}
$CleanupJob = Start-Job -ScriptBlock $CleanupBlock
$Null = Wait-Job $CleanupJob | Receive-Job
Remove-Job $CleanupJob
# Force the Working Set trim from the host side
$Signatures = '[DllImport("psapi.dll")] public static extern bool EmptyWorkingSet(IntPtr hProcess);'
$Win32API = Add-Type -MemberDefinition $Signatures -Name "Win32PSAPI_GC" -Namespace "Win32Functions" -PassThru
$Win32API::EmptyWorkingSet($TargetProcess.Handle) | Out-Null
# Refresh metrics to show the real drop
$TargetProcess.Refresh()
$NewTotalWS = [Math]::Round($TargetProcess.WorkingSet64 / 1MB, 2)
Write-Host "`nGarbage collection and Working Set trim complete!" -ForegroundColor Green
Write-Host "New Working Set (Memory): ${NewTotalWS} MB" -ForegroundColor Green
Write-Host "Actual RAM Recovered: $([Math]::Round($TotalWS_Before - $NewTotalWS, 2)) MB" -ForegroundColor Green
} else {
Write-Host "Cleanup skipped. Exiting diagnostic tool." -ForegroundColor Gray
}