DEV Community

Cover image for Solved: Stop Storing Results in Variables. Pipe Them Instead.
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Stop Storing Results in Variables. Pipe Them Instead.

🚀 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 of foreach loops 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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.


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)