Minimal progress view re-renders all active bars on every update, causing O(N^2) console output when bars accumulate
Summary
When cmdlets write progress bars with unique Activity IDs and don't call Write-Progress -Completed, the Minimal progress view (default since PS 7.2) accumulates all bars in its pending state. Each subsequent progress update re-renders the entire set of active bars inline, so with N accumulated bars, the total console output grows as O(N²). With 150+ bars (common in Azure blob downloads), the console scrolls uncontrollably and becomes unusable.
The Classic view handles this gracefully -- it renders bars in a fixed pane at the top of the console and only shows the most recent bars. The Minimal view has no such limit.
Environment
PSVersion: 7.4.x / 7.5.x (any PS 7.2+)
OS: Windows 11 (also reproducible on Linux/macOS)
$PSStyle.Progress.View: Minimal (default)
Steps to reproduce
Minimal repro (10 seconds)
# Each iteration adds a new progress bar that is never completed.
# Minimal view re-renders ALL accumulated bars on every update.
# By bar 50, the console is scrolling rapidly with dozens of stacked bars.
1..200 | ForEach-Object {
Write-Progress -Id $_ -Activity "Download blob blob_$_.dat" `
-Status "[Transmit successfully.]" -PercentComplete 100
Start-Sleep -Milliseconds 50
}
Expected behavior
Each new progress bar should replace the previous one (or at minimum, bars at 100% should auto-expire). The console should show one bar at a time -- the most recently updated one.
Actual behavior
All 200 bars accumulate on screen. Each progress update re-renders the full set -- the Minimal view erases and redraws all active bars plus a full screen of blank space below them on every update. With N accumulated bars, each update writes N bar lines plus a screenful of blanks, producing massive console output. Here is what the output looks like as bars accumulate:
Download blob blob_1.dat [[Transmit successfully.] ]
Download blob blob_2.dat [[Transmit successfully.] ]
Download blob blob_3.dat [[Transmit successfully.] ]
Download blob blob_4.dat [[Transmit successfully.] ]
Download blob blob_5.dat [[Transmit successfully.] ]
Download blob blob_6.dat [[Transmit successfully.] ]
Download blob blob_7.dat [[Transmit successfully.] ]
Download blob blob_8.dat [[Transmit successfully.] ]
Download blob blob_9.dat [[Transmit successfully.] ]
Download blob blob_10.dat [[Transmit successfully.] ]
...
(all bars stacked and re-rendered on every update, scrolling the console)
Contrast with Classic view
The same script with $PSStyle.Progress.View = 'Classic' renders bars in a fixed pane at the top of the console and does not cause scrolling. The Classic view already handles this scenario gracefully.
Real-world trigger
Azure PowerShell cmdlets like Get-AzStorageBlobContent internally use PSCmdlet.WriteProgress() with a unique Activity ID per blob and never call -Completed. When downloading 150+ blobs, the Minimal view makes the console unusable.
This pattern is common -- cmdlet authors intentionally omit -Completed because they want users to see the bar at 100% rather than having it vanish immediately. Issue #3366 (item 6) identified this design tension back in 2017:
The reason this keeps happening is that the cmdlet author wants to show 100% percent complete to the user. Using
-Completedimmediately removes the progress bar, so the user never sees the final state.
The Minimal view turns this common pattern from a minor cosmetic annoyance (one lingering bar) into a severe usability bug (hundreds of stacked bars causing console flood).
Root cause analysis
The Minimal progress view renders all active ProgressRecord entries inline at the bottom of the console on every progress update. Unlike the Classic view which uses a fixed pane, the Minimal view has:
- No upper limit on the number of bars rendered simultaneously
- No auto-expiry for bars that have reached 100% completion
- No deduplication or replacement logic for sequential bars in the same scope
When N bars accumulate without being completed, each new Write-Progress call triggers a full re-render of all N bars, producing O(N²) total console output over the lifetime of the operation.
Suggested fixes
Any of these would resolve the issue:
Auto-complete progress bars that reach 100%. A bar at
PercentComplete = 100is logically done. The runtime could auto-complete it after a short delay (e.g. 1 second), removing it from the pending state. This directly addresses the common cmdlet pattern of showing 100% without calling-Completed.Cap the number of visible progress bars. The Minimal view should render at most N bars (e.g. 5-10) and silently complete the oldest when the limit is exceeded. This prevents unbounded accumulation regardless of the cause.
Add a "replace" semantics option. A new bar with a new Activity ID in the same scope could automatically replace the previous bar (similar to how Classic view effectively handles it). This could be opt-in via a parameter or automatic for bars with the same
ParentId.
Workaround
We implemented a Write-Progress proxy function that tracks active bars and auto-completes sibling bars (same ParentId) that have reached 100% when a new bar arrives. This prevents accumulation while preserving concurrent and parent/child progress bar scenarios. However, this is fragile -- it can't intercept C# cmdlets that call PSCmdlet.WriteProgress() directly via the host API (those bypass the PowerShell function entirely), so we also needed reflection-based cleanup of PendingProgress._topLevelNodes after command execution.
A proper fix in the engine would eliminate the need for these workarounds.
Related issues
- #3366 -- Progress bar problems tracking issue (item 6 identifies the cmdlet author motivation for omitting
-Completed) - #21269 --
Test-NetConnectionprogress bar outstays its welcome (single-bar instance of the same root cause) - #12541 -- Progress bar remains despite Completed record (SourceId mismatch variant, pre-Minimal view)
- #10900 -- Progress bar can significantly impact cmdlet performance (related performance concern)
Source: PowerShell/PowerShell