PowerShell Measure Script Performance with Measure-Command

When a script takes ten minutes to complete and you suspect one section is the bottleneck, guessing wastes time. Measure-Command gives you precise timing data for any block of PowerShell code, making it the first tool to reach for when you want to PowerShell measure script performance. This post shows you how to benchmark individual commands, compare two approaches, profile large scripts section by section, and interpret the results correctly.
Quick Answer / TL;DR
Wrap any code in Measure-Command { ... } and check the .TotalSeconds or .TotalMilliseconds property on the returned TimeSpan object. Do a warm-up run first to exclude module loading time.
Basic Measure-Command Usage
Measure-Command executes the script block you provide and returns a TimeSpan object representing the elapsed time. The script block output is discarded — you see only timing data. Access specific time intervals through properties like .TotalSeconds, .TotalMilliseconds, or .Minutes.
# Time a simple command
$elapsed = Measure-Command {
Get-ChildItem -Path C:\Windows -Recurse -ErrorAction SilentlyContinue
}
Write-Host "Elapsed: $($elapsed.TotalSeconds) seconds"
Elapsed: 3.847 seconds
Benchmark Two Different Approaches
The most common use of Measure-Command is comparing two implementations of the same operation. Store both results in variables and compute the ratio to understand the relative speed difference.
$data = 1..10000
# Approach A: ForEach-Object cmdlet
$timeA = Measure-Command {
$result = $data | ForEach-Object { $_ * 2 }
}
# Approach B: .ForEach() method
$timeB = Measure-Command {
$result = $data.ForEach({ $_ * 2 })
}
Write-Host "ForEach-Object: $($timeA.TotalMilliseconds) ms"
Write-Host ".ForEach() method: $($timeB.TotalMilliseconds) ms"
Write-Host "Ratio: $([math]::Round($timeA.TotalMilliseconds / $timeB.TotalMilliseconds, 2))x"
Run Multiple Iterations for Accuracy
A single measurement is unreliable due to background process noise, disk cache state, and JIT compilation. Run five or more iterations and average the results. The first run is often slower due to module loading and .NET JIT warm-up — exclude it or run a throwaway warm-up call first.
# Warm-up run to load modules and JIT
Measure-Command { Get-Process } | Out-Null
# Timed runs
$times = 1..5 | ForEach-Object {
(Measure-Command { Get-Process | Where-Object CPU -gt 10 }).TotalMilliseconds
}
$avg = ($times | Measure-Object -Average).Average
Write-Host "Average over 5 runs: $([math]::Round($avg, 1)) ms"
Identify Pipeline vs Loop Performance
Pipeline streaming and foreach loops have different performance profiles. Pipelines have per-object overhead but constant memory usage. Loops loading all objects first are faster for small collections but consume more memory. Measure both in the context of your actual data size.
$files = Get-ChildItem C:\Logs -Recurse -File
# Pipeline approach
$pipelineTime = Measure-Command {
$large = $files | Where-Object Length -gt 1MB
}
# Foreach loop approach
$loopTime = Measure-Command {
$large = foreach ($f in $files) {
if ($f.Length -gt 1MB) { $f }
}
}
Write-Host "Pipeline: $($pipelineTime.TotalMilliseconds) ms"
Write-Host "Loop: $($loopTime.TotalMilliseconds) ms"
Profile Large Scripts Section by Section
For scripts with many distinct phases, wrap each section individually to find the slow one. Name the sections clearly so the output tells you exactly where time is spent.
$profile = @{}
$profile['Load AD Users'] = (Measure-Command {
$users = Get-ADUser -Filter * -Properties Department,Mail
}).TotalSeconds
$profile['Filter Active'] = (Measure-Command {
$active = $users | Where-Object Enabled -eq $true
}).TotalSeconds
$profile['Export CSV'] = (Measure-Command {
$active | Export-Csv C:\Reports\users.csv -NoTypeInformation
}).TotalSeconds
$profile.GetEnumerator() | Sort-Object Value -Descending |
Format-Table Name, @{L='Seconds';E={[math]::Round($_.Value,2)}} -AutoSize
Common Quick Wins
After measuring, common performance improvements include: filtering at the source with -Filter instead of Where-Object; using .Where() method instead of Where-Object for in-memory collections; avoiding repeated identical cmdlet calls inside a loop; and using -AsJob or ForEach-Object -Parallel (PS7) for independent operations. Always measure before and after any optimization to confirm improvement.
Common Errors and Fixes
- First run slower due to module loading — warm-up run first. The first call to any cmdlet loads its module into the PowerShell session. This adds hundreds of milliseconds to the first measurement. Always run one throwaway iteration before your timed runs by calling
Measure-Command { ... } | Out-Null. - Measure-Command output is a TimeSpan object — access .TotalSeconds. If you print the raw result, you see a formatted TimeSpan string like
00:00:03.8470000. For numeric comparisons and calculations, use.TotalSeconds,.TotalMilliseconds, or.TotalMinutesto get aDouble.
Related Cmdlets / See Also
Wrapping Up
Measure-Command turns performance tuning from guesswork into data-driven decisions. Always warm up, run multiple iterations, and compare results numerically via .TotalMilliseconds. Once you identify the slow section, the optimization is usually straightforward — filter earlier, avoid redundant calls, or switch to method syntax.


