đ Executive Summary
TL;DR: Storing large command outputs in variables can exhaust memory and crash servers by loading all data into RAM simultaneously. Instead, leverage the PowerShell pipeline to stream data object by object, ensuring efficient, low-memory processing for large datasets.
đŻ Key Takeaways
- Storing command results in variables (like a âbucketâ) loads all objects into memory, causing memory exhaustion for large datasets.
- The PowerShell pipeline acts as a âconveyor belt,â processing data object by object, maintaining a small, constant, and predictable memory footprint.
- The âFilter Leftâ principle dictates performing filtering as early as possible at the source (e.g., using native cmdlet -Filter parameters) to minimize data transfer and memory usage.
- For massive datasets or when re-processing is needed, spooling data to disk (e.g., Export-Csv then Import-Csv) allows streaming processing with near-zero memory footprint.
- Using
ForEach-Object(or its alias%) directly with piped input ensures one-at-a-time processing, avoiding the memory overhead offoreachloops on pre-loaded variables.
Stop storing massive command outputs in variables. Instead, learn to love the pipeline; it streams data object by object, preventing memory exhaustion and catastrophic script failures on your production servers.
The Pipeline is Your Friend: Why Storing Command Results Will Crash Your Servers
I remember it like it was yesterday. 3:00 PM on a Friday. A junior engineer, bless his heart, was tasked with a âsimpleâ cleanup script: find and log all temp files older than 30 days across our web farm. He wrote a one-liner, something like $files = Get-ChildItem -Path \\web-cluster-*-c$\temp -Recurse, and ran it. Ten minutes later, my pager goes off. One by one, our entire production web fleet, prod-web-01 through prod-web-20, started throwing memory pressure alerts and falling over. The culprit? His script was trying to load millions of file objects from 20 servers into a single variable in memory on his management box, which then cascaded into a resource nightmare. Weâve all been there. Itâs a classic mistake born from thinking procedurally instead of thinking in streams. The quote from that Reddit thread hit home: âwe donât recommend storing the results in a variable.â Let me tell you why theyâre right.
The âWhyâ: Variables are Buckets, Pipelines are Conveyor Belts
When you execute a command like $myBigList = Get-ADUser -Filter *, you are telling PowerShell, âGo find every single user in Active Directory, create an object for each one, and donât come back until youâve collected them all in this giant memory bucket called $myBigList.â If you have 50,000 users, youâre now holding 50,000 objects in RAM. This is fine for a few dozen or even a few hundred items. It is catastrophic for thousands or millions.
The pipeline, on the other hand, is a conveyor belt. When you run Get-ADUser -Filter * | Where-Object {$_.Enabled -eq $false}, PowerShell gets the first user object, puts it on the belt, and sends it to the Where-Object command. That command inspects it, decides if it passes the test, and either puts it back on the belt for the next command or discards it. It then signals back, âReady for the next one!â This happens one object at a time. The memory footprint is tiny, constant, and predictable, regardless of whether youâre processing 100 objects or 10 million.
Pro Tip from the Trenches: Think of it this way. A variable collects everything before you can do anything. A pipeline lets you process items as they arrive. For large-scale automation, the second approach is the only one that scales without bringing down a server.
Three Ways to Tame the Data Stream
So, how do we fix this without giving up and doing everything by hand? Here are the three approaches we teach our engineers at TechResolve, from the quick fix to the âbreak glass in case of emergencyâ option.
Solution 1: The Obvious Fix â Just Use the Pipeline!
This is the most direct solution. Instead of saving to a variable and then iterating over it with a foreach loop, pipe the output directly to a ForEach-Object loop (or its alias %). This ensures one-at-a-time processing.
The Bad Way (The Memory Hog):
# WARNING: This will load ALL VMs into memory first!
$allVMs = Get-VM -ComputerName prod-hyperv-cluster
foreach ($vm in $allVMs) {
if ($vm.State -eq 'Off') {
Write-Host "$($vm.Name) is currently off. Removing snapshot."
Get-VMSnapshot -VMName $vm.Name | Remove-VMSnapshot
}
}
The Good Way (The Stream):
# This processes one VM at a time. Beautiful.
Get-VM -ComputerName prod-hyperv-cluster | ForEach-Object {
if ($_.State -eq 'Off') {
Write-Host "$($_.Name) is currently off. Removing snapshot."
# Note the use of $_ to represent the current object in the pipeline
Get-VMSnapshot -VMName $_.Name | Remove-VMSnapshot
}
}
Solution 2: The Smart Fix â Filter Left, Process Right
This is an extension of using the pipeline, but itâs a critical architectural principle. Do your filtering as early (as far to the âleftâ in the command) as possible. Donât pull 50,000 AD users across the network just to discard 49,000 of them on your machine. Let the source server (like a Domain Controller or a database) do the heavy lifting.
The Inefficient Way (Filtering Late):
# Pulls ALL users, then filters. Bad for network and memory.
Get-ADUser -Filter * -Properties LastLogonDate | Where-Object {
$_.Enabled -eq $false -and $_.LastLogonDate -lt (Get-Date).AddDays(-90)
} | Select-Object Name
The Efficient Way (Filtering Left):
# The Domain Controller does the filtering. Only the results are sent.
$ninetyDays = (Get-Date).AddDays(-90).ToFileTime()
Get-ADUser -Filter {
Enabled -eq $false -and LastLogonTimestamp -lt $ninetyDays
} -Properties LastLogonTimestamp | Select-Object Name
By using the cmdletâs native -Filter parameter, youâre asking the AD server to find only the users that match. The number of objects that ever even enter the pipeline is drastically smaller from the very beginning.
Solution 3: The âBreak Glassâ Fix â Spool to Disk
Sometimes you face a truly massive dataset, and you might need to re-process it multiple times. Maybe the source API is slow, and you donât want to query it repeatedly. Holding it in memory is not an option. The solution? Dump the data to a temporary file on disk.
This is a âhackyâ but incredibly effective method. You take the one-time performance hit of writing everything to a file (like a CSV or JSONL), but then you can read that file back line-by-line, which has a near-zero memory footprint.
The Process:
# Step 1: Export the massive dataset to a file. This can still be slow.
# Export-Csv is great because it handles objects cleanly.
Get-VeryLargeDataset -Server prod-db-01 | Export-Csv -Path C:\temp\dataset.csv -NoTypeInformation
# Step 2: Process the data by streaming it from the file.
# Import-Csv streams the records one by one if piped.
Import-Csv -Path C:\temp\dataset.csv | ForEach-Object {
# Now you can work with each row ($_), one at a time.
# The entire file is NOT loaded into memory.
if ($_.Status -eq 'Failed') {
Invoke-MyRetryLogic -ID $_.TransactionID
}
}
# Step 3: Clean up after yourself!
Remove-Item -Path C:\temp\dataset.csv
| Method | Pros | Cons |
|---|---|---|
| 1. Pipelining | Extremely low memory usage; idiomatic; fast for most tasks. | Data is transient; canât easily re-process without re-running the initial command. |
| 2. Filtering Left | Most efficient method; reduces memory, CPU, and network load. | Relies on the source command having robust server-side filtering capabilities. |
| 3. Spooling to Disk | Handles virtually infinite data sizes; data is persistent for re-processing. | Slowest method due to disk I/O; requires temporary disk space; more complex code. |
So, the next time you start to type $results = ..., pause for a second. Ask yourself: âHow many items could this command possibly return?â If the answer is âI donât knowâ or âa lot,â do yourself and your servers a favor. Ditch the variable and embrace the pipeline. Your future self, who isnât getting paged at 3:00 PM on a Friday, will thank you.
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)