PowerShell ForEach vs .ForEach(): Method Syntax Speed Test

When you iterate over thousands of objects in PowerShell, your choice between .ForEach() method and ForEach-Object cmdlet has measurable performance consequences. Understanding PowerShell ForEach method vs cmdlet performance lets you make an informed trade-off between speed, memory usage, and streaming capability. This post benchmarks both, explains why the difference exists, and gives you a clear decision guide.
Quick Answer / TL;DR
For in-memory collections, .ForEach() is typically 2–5x faster than ForEach-Object. Use ForEach-Object when you need streaming (one-at-a-time pipeline processing) or when memory is limited.
.ForEach() Method Syntax
The .ForEach() method is available on any PowerShell array or collection. It takes a script block, iterates over each element, and returns results as a generic list. The method bypasses the PowerShell pipeline infrastructure, which is why it is faster for in-memory operations.
# .ForEach() method — faster for in-memory collections
$numbers = 1..10000
$doubled = $numbers.ForEach({ $_ * 2 })
# Return objects with transformation
$files = Get-ChildItem C:\Logs -Filter *.log
$summaries = $files.ForEach({
[PSCustomObject]@{
Name = $_.Name
SizeMB = [math]::Round($_.Length / 1MB, 3)
}
})
ForEach-Object Cmdlet Syntax
ForEach-Object (alias %) is a pipeline cmdlet. It receives objects one at a time through the pipeline and processes them individually. This streaming behavior means the entire collection is never loaded into memory at once — memory usage is proportional to one object, not the whole collection.
# ForEach-Object cmdlet — streaming pipeline processing
$numbers = 1..10000
$doubled = $numbers | ForEach-Object { $_ * 2 }
# Real-world pipeline: streams from file without loading all lines
Get-Content C:\Logs\large.log |
ForEach-Object {
if ($_ -match '\[ERROR\]') { $_ }
} |
Out-File C:\Logs\errors_only.log
Benchmark Results
Benchmark both approaches on the same data to see the real difference. The gap widens as the collection grows. For small collections under a few hundred items, the difference is negligible. For collections of 10,000+ objects, the method syntax consistently wins.
$data = 1..50000
# Warm up
$data | ForEach-Object { $_ } | Out-Null
$data.ForEach({ $_ }) | Out-Null
$cmdletTime = (Measure-Command {
$r = $data | ForEach-Object { $_ * 2 }
}).TotalMilliseconds
$methodTime = (Measure-Command {
$r = $data.ForEach({ $_ * 2 })
}).TotalMilliseconds
Write-Host "ForEach-Object: $([math]::Round($cmdletTime,1)) ms"
Write-Host ".ForEach() method: $([math]::Round($methodTime,1)) ms"
Write-Host "Method is $([math]::Round($cmdletTime/$methodTime,1))x faster"
ForEach-Object: 312.4 ms
.ForEach() method: 78.1 ms
Method is 4.0x faster
When Pipeline Streaming Wins
The .ForEach() method requires the entire collection to be in memory before processing starts. For large file streams, large query result sets, or situations where you only need the first few results, pipeline streaming with ForEach-Object is the right choice — it can process gigabytes of data without memory pressure.
# Pipeline wins here: file is streamed, not loaded into RAM
Get-Content C:\Logs\10gb_access.log | ForEach-Object {
if ($_ -match '500') { $_ }
} | Select-Object -First 100 | Out-File C:\Logs\server_errors.log
# Method loses here: loading 10GB file just to call .ForEach() is impractical
# (Get-Content -Raw C:\Logs\10gb_access.log).Split("`n").ForEach({...}) # Bad!
Memory Considerations
The .ForEach() method stores the entire input collection in memory before iterating. For 100,000 complex objects, this can be hundreds of megabytes. ForEach-Object in a pipeline holds only the current object in memory at once. For large datasets on memory-constrained systems, always stream with ForEach-Object.
# Check memory impact
[gc]::Collect()
$before = [System.GC]::GetTotalMemory($true)
# Method: loads all objects
$items = Get-ChildItem C:\Windows -Recurse -ErrorAction SilentlyContinue
$result = $items.ForEach({ $_.FullName })
$after = [System.GC]::GetTotalMemory($false)
Write-Host "Memory used: $([math]::Round(($after - $before)/1MB, 2)) MB"
Recommendation Guide
- Use
.ForEach()method when: the collection is already in a variable, speed matters, and the dataset fits comfortably in memory (under ~500MB of objects). - Use
ForEach-Objectcmdlet when: the data streams from a file or cmdlet, memory is limited, you need early termination (Select-Object -First N), or you are chaining multiple pipeline stages. - Use the
foreachkeyword when: you need to break out of the loop early withbreak, which works inforeach (...) { }but not directly inForEach-Object.
Common Errors and Fixes
- .ForEach() loads all objects into memory — streaming impossible. If you use
(Get-Content file.txt).ForEach({...})on a large file, PowerShell loads the entire file into an array before iterating. UseGet-Content file.txt | ForEach-Object {...}for streaming behavior. - Method syntax not available on all collection types. The
.ForEach()method is available on PowerShell arrays and generic collections. It is not available on all .NET enumerables. If you get “Method invocation failed because [X] does not contain a method named ‘ForEach'”, pipe toForEach-Objectinstead, or collect to an array first with@(...).
Related Cmdlets / See Also
Wrapping Up
For in-memory collections where speed matters, .ForEach() is the clear winner. For streaming data from files or cmdlets, ForEach-Object is correct. The foreach keyword is best when you need break/continue semantics. Benchmark with Measure-Command on your actual data size before committing to either approach in production.


