Speeding Up I/O Loops with PowerShell 7 Parallelism
Learn how to use ForEach-Object -Parallel in PowerShell 7 to eliminate I/O bottlenecks, handle variable scoping with $using:, and tune concurrency with ThrottleLimit.
19 Aug 2025, 13:06 UTC

The Bottleneck of Sequential I/O
When you use a standard ForEach-Object loop to perform network requests or file system checks, your script spends most of its time waiting. If you are querying 100 API endpoints and each takes one second to respond, your script will take at least 100 seconds to finish, regardless of how powerful your CPU is. This is an I/O-bound problem: the CPU is idle while waiting for the network or disk to respond.
PowerShell 7 introduced the -Parallel parameter for ForEach-Object, allowing you to execute multiple iterations of a loop concurrently. This transforms a linear wait into a parallel one, significantly reducing the total wall-clock time for tasks that involve waiting on external resources.
How Parallel Runspaces Work
Unlike a standard loop that processes items one by one in the main thread, -Parallel creates separate runspaces. A runspace is an isolated environment that contains its own state, variables, and PowerShell engine instance. Because these runspaces operate independently, PowerShell can trigger multiple operations simultaneously.
To prevent your system from being overwhelmed by hundreds of simultaneous threads, the -ThrottleLimit parameter controls the maximum number of concurrent runspaces. By default, this is set to 5. If you are processing 1,000 items with a throttle limit of 5, PowerShell will start 5 iterations; as soon as one finishes, it starts the next until the queue is empty.
Handling Variable Scope with $using:
Because each parallel iteration runs in its own isolated runspace, it cannot see variables defined in the parent script. If you try to reference a variable from the main script inside a -Parallel block, it will be null.
To pass data into the parallel block, you must use the $using: scope modifier. This tells PowerShell to serialize the value of the variable from the parent scope and provide it to the child runspace.
Worked Example: Concurrent API Health Checks
The following example demonstrates how to check the status of multiple URLs concurrently. This script must be run in PowerShell 7.0 or higher. To verify your version, run $PSVersionTable.PSVersion in your terminal.
# Run this in a PowerShell 7+ terminal
$urls = @(
\"https://www.google.com\",
\"https://www.microsoft.com\",
\"https://www.github.com\",
\"https://www.reddit.com\",
\"https://www.stackoverflow.com\"
)
$timeoutSeconds = 2
# We use -Parallel to check URLs concurrently
# We use $using:timeoutSeconds to pass the parent variable into the runspace
$results = $urls | ForEach-Object -Parallel {
try {
$response = Invoke-WebRequest -Uri $_ -TimeoutSec $using:timeoutSeconds -UseBasicParsing
[PSCustomObject]@{
URL = $_
Status = \"Up\"
Code = $response.StatusCode
}
} catch {
[PSCustomObject]@{
URL = $_
Status = \"Down\"
Code = \"Error\"
}
}
} -ThrottleLimit 5
$results | Format-Table
Execution Details
- Permissions: Standard user permissions are sufficient unless the target URLs require specific authentication.
- Expected Result: A table showing the status of all URLs. The total execution time will be roughly equal to the slowest single request, rather than the sum of all requests.
- Risk: Setting
-ThrottleLimittoo high (e.g., 100+) can lead to socket exhaustion or cause the remote server to flag your IP for a Denial-of-Service (DoS) attack.
Trade-offs and Limitations
Parallelism is not a universal performance booster. There are three critical limitations to consider:
1. Non-Deterministic Order
Because iterations finish at different times, the output order is not guaranteed to match the input order. If your downstream logic depends on the sequence of the $urls array, you must add a sequence index to your objects and sort them after the loop finishes.
2. Overhead for CPU-Bound Tasks
Creating and managing runspaces consumes memory and CPU cycles. If the task inside the loop is very fast (e.g., simple math or string manipulation), the overhead of managing the runspaces will likely make the -Parallel loop slower than a sequential one.
3. Thread Safety and Shared State
Runspaces cannot safely write to the same variable in the parent scope. For example, you cannot use $using:counter++ to track progress because the variable is passed by value, not by reference. To collect data, let the parallel block Write-Output the result and capture the entire loop's output into a variable, as shown in the example above.
Verification and Rollback
To verify if -Parallel is providing a benefit, wrap your loop in Measure-Command { ... }. Compare the TotalSeconds of a standard ForEach-Object loop against one using -Parallel.
Since this operation only reads data and does not change system state, no rollback is required. However, if you are using -Parallel to modify files, ensure you are not writing to the same file simultaneously from different runspaces, as this will cause file-lock errors.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.