#26915·PowerShell

Minimal progress view re-renders all active bars on every update, causing O(N^2) console output when bars accumulate

Author: ayrtonmasseyCreated Feb 27, 2026Updated Sep 16, 2026
LabelsWG-Interactive-Console

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 -Completed immediately 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:

  1. No upper limit on the number of bars rendered simultaneously
  2. No auto-expiry for bars that have reached 100% completion
  3. 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:

  1. Auto-complete progress bars that reach 100%. A bar at PercentComplete = 100 is 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.

  2. 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.

  3. 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-NetConnection progress 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)