PowerShell Pipeline: Where-Object vs .Where() Method Performance

Two Ways to Filter, One Clear Winner for In-Memory Data
PowerShell gives you two syntactically different ways to filter a collection: the Where-Object cmdlet and the .Where() intrinsic method on arrays. They produce identical output for common cases, but their performance characteristics diverge significantly once the collection is already loaded into memory. Knowing which to reach for — and when the pipeline version is still the right choice — is the kind of judgment that separates scripts that complete in seconds from those that grind through minutes of unnecessary overhead.
Quick Answer
Use .Where() on in-memory collections for significantly faster filtering. Use Where-Object in pipelines that stream data from disk, the network, or cmdlets — where loading everything into memory first would cost more than the filter speed saves.
How Where-Object Works in the Pipeline
Where-Object is a streaming cmdlet. It processes one object at a time as each arrives from the pipeline, evaluates the script block or comparison expression, and passes matching objects downstream. This is memory-efficient for large data sources because the entire collection never has to exist in memory simultaneously.
# Streaming: reads each file from disk one at a time, filters, outputs
Get-ChildItem -Path 'C:\Logs' -Recurse |
Where-Object { $_.Extension -eq '.log' -and $_.Length -gt 1MB } |
Select-Object Name, Length, LastWriteTime
The pipeline here is genuinely streaming: Get-ChildItem emits objects as it enumerates the filesystem, Where-Object filters each one before the next arrives. Peak memory stays flat regardless of how many files exist.
The .Where() Method Syntax on PowerShell Arrays
.Where() is an intrinsic method available on all PowerShell array-like collections. It takes a script block (and optionally a mode and a count limit) and returns a new array of matching elements. Because it operates on an already-materialized array, there is no streaming benefit — but the lower per-element overhead makes it much faster when the collection is already in memory.
$processes = Get-Process # loads all processes into memory first
# .Where() method — operates on the in-memory array
$highCpu = $processes.Where({ $_.CPU -gt 100 })
# Equivalent Where-Object pipeline form
$highCpu = $processes | Where-Object { $_.CPU -gt 100 }
# Both produce identical results; .Where() is faster for this case
$highCpu | Select-Object Name, CPU, Id | Format-Table -AutoSize
Benchmark: Where-Object vs .Where() on 100k Objects
The performance gap is measurable and consistent. Below is a representative benchmark using Measure-Command on a collection of 100,000 custom objects.
$data = 1..100000 | ForEach-Object {
[PSCustomObject]@{ Id = $_; Value = Get-Random -Maximum 1000 }
}
$woTime = (Measure-Command {
$null = $data | Where-Object { $_.Value -gt 500 }
}).TotalMilliseconds
$wmTime = (Measure-Command {
$null = $data.Where({ $_.Value -gt 500 })
}).TotalMilliseconds
Write-Host "Where-Object : ${woTime}ms"
Write-Host ".Where() : ${wmTime}ms"
Write-Host "Speedup : $([math]::Round($woTime / $wmTime, 1))x"
Where-Object : 1842ms
.Where() : 218ms
Speedup : 8.4x
The speedup comes from avoiding per-object pipeline overhead. Where-Object must wrap and unwrap each object in pipeline machinery; .Where() operates on the array directly with far less ceremony per element.
Using .Where() Modes: Default, First, Last, Until, SkipUntil, Split
.Where() accepts an optional second argument that changes the filtering behavior beyond simple match-all. These modes make certain patterns that would require multiple pipeline stages expressible as a single call.
$numbers = 1..20
# Default — return all matching elements
$numbers.Where({ $_ % 2 -eq 0 }) # 2,4,6,...,20
# First — return first N matches (third arg is count)
$numbers.Where({ $_ % 2 -eq 0 }, 'First', 3) # 2,4,6
# Last — return last N matches
$numbers.Where({ $_ % 2 -eq 0 }, 'Last', 2) # 18,20
# Until — return elements until condition first becomes true
$numbers.Where({ $_ -gt 5 }, 'Until') # 1,2,3,4,5
# SkipUntil — skip elements until condition becomes true, return rest
$numbers.Where({ $_ -gt 15 }, 'SkipUntil') # 16,17,18,19,20
# Split — returns TWO arrays: [matched],[unmatched]
$matched, $unmatched = $numbers.Where({ $_ -gt 10 }, 'Split')
Write-Host "Matched : $matched" # 11-20
Write-Host "Unmatched: $unmatched" # 1-10
The Split mode is particularly useful when you need both the passing and failing items from a single filter operation without iterating the collection twice.
Chaining .Where() and .ForEach() for LINQ-Style Queries
Both .Where() and .ForEach() return arrays, so they can be chained directly without intermediate pipeline overhead. This produces LINQ-like query chains that are both readable and fast.
$services = Get-Service
# Filter to running services, project name and display name, sort
$runningNames = $services
.Where({ $_.Status -eq 'Running' })
.ForEach({ $_.DisplayName })
# Check version first — .Where() requires PS4+
if ($PSVersionTable.PSVersion.Major -ge 4) {
$runningNames | Sort-Object | Select-Object -First 10
}
Choosing the Right Tool for Streaming vs In-Memory Data
The decision is straightforward: if data is streaming from a cmdlet, the filesystem, or the network — use Where-Object to avoid materializing the full collection. If the collection is already in a variable, use .Where() for speed. A hybrid approach — stream data into a variable first with $data = Get-SomeCmdlet — is only worth it when you need to filter the same collection multiple times and the collection fits comfortably in memory.
Common Errors
.Where()is not available in PowerShell 2.0. The intrinsic methods were introduced in PowerShell 4.0. Scripts targeting legacy systems still running WMF 2.0 must useWhere-Object. Check with$PSVersionTable.PSVersion.Major -ge 4before using.Where()in cross-version scripts.- Calling
.Where()on$nullthrows MemberInvocationException. If the variable holding your collection might be$null(e.g., a cmdlet returned nothing), guard withif ($null -ne $data)or use the null-conditional approach in PS 7+. UnlikeWhere-Object, which returns nothing when given a null input,.Where()will throw when the method is called on a null reference.
Related Cmdlets / See Also
Wrapping Up
For already-loaded arrays, .Where() is consistently 5–10x faster than Where-Object and offers additional modes like Split, First, and Until that eliminate extra pipeline stages. Keep Where-Object for genuine pipeline streaming where memory efficiency matters more than filtering speed, and reach for .Where() everywhere else.


