When Your Script Blocks for 20 Minutes
A managed services client we support runs a Windows environment across three sites — roughly 400 endpoints. Their network team had a PowerShell script that pinged every node, pulled WMI data, and exported results to CSV. The problem: the script ran synchronously. One host at a time. If a node was offline and Test-Connection sat waiting for its timeout, the whole script stalled. They were getting 25-minute run times for a task that should finish in under three minutes.
The fix was one parameter: the PowerShell -AsJob parameter.
What -AsJob Actually Does
When you append -AsJob to a supported cmdlet, PowerShell immediately returns a job object instead of waiting for the command to finish. The prompt comes back right away. The work keeps running in the background. You retrieve results when you’re ready — not when PowerShell is done blocking your session.
Think of it as fire-and-forget with a receipt. You get a job ID, a name, and a state. The actual execution happens asynchronously, and you collect output later with Receive-Job.
One nuance worth knowing: when -AsJob targets remote computers, the job object is created on the local machine even though execution happens on the remote host. Results automatically return to the local session when you call Receive-Job. This is functionally similar to using Invoke-Command wrapped around Start-Job, but with cleaner syntax for cmdlets that natively support the parameter.
Which Cmdlets Support -AsJob?
Not every cmdlet supports -AsJob. To see exactly what’s available in your environment, run this:
Get-Help * -Parameter AsJob | Sort-Object name | Format-Table name, synopsis -AutoSize -Wrap
Network-related cmdlets like Test-Connection, Get-NetAdapter, and various WMI-backed networking cmdlets on Windows Server 2022 and 2025 natively support it. If a cmdlet doesn’t, you fall back to wrapping in Start-Job. Either way, the async pattern holds.
The Job Lifecycle: Four Cmdlets You Need
Managing background jobs follows a predictable lifecycle. Four cmdlets cover 90% of production use cases:
- Get-Job — Lists all jobs in the current session, with state (Running, Completed, Failed)
- Receive-Job — Pulls results from a completed or in-progress job
- Wait-Job — Blocks until a specific job finishes, useful when downstream logic depends on the result
- Remove-Job — Cleans up after yourself. Don’t skip this step.
A Practical Network Sweep Example
Here’s the pattern applied to that client’s environment. Sweep multiple remote hosts concurrently, collect results when done:
# Fire off connectivity checks across all nodes — don't wait for each one
$nodes = @('SRV-DC01', 'SRV-FS02', 'SRV-APP03', 'SRV-DB04', 'SRV-MGMT05')
$jobs = foreach ($node in $nodes) {
Test-Connection -ComputerName $node -Count 2 -AsJob
}
# Wait for all jobs to finish
Wait-Job -Job $jobs | Out-Null
# Collect all results
$results = $jobs | Receive-Job
# Clean up — orphaned jobs accumulate and consume memory
$jobs | Remove-Job
$results | Select-Object Address, StatusCode, ResponseTime
That client’s 25-minute runtime dropped to under two minutes. The job count was the only variable. That’s the kind of toil reduction worth measuring and documenting.
Combining -AsJob with WMI for Remote Data Collection
Network connectivity checks are one thing. Where -AsJob really earns its keep is WMI operations across remote systems. Pulling hardware inventory, checking driver versions, or querying NIC configuration from 50 hosts synchronously is just operational drag. Running it async is standard engineering.
# Pull NIC configuration from multiple remote hosts in parallel
$servers = Get-Content -Path 'C:\ops\server-list.txt'
$wmiJobs = foreach ($server in $servers) {
Invoke-Command -ComputerName $server -ScriptBlock {
Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'IPEnabled = True'
} -AsJob -JobName "NIC_$server"
}
Wait-Job -Job $wmiJobs | Out-Null
foreach ($job in $wmiJobs) {
$data = Receive-Job -Job $job
Write-Output "$($job.Name): $($data.IPAddress)"
Remove-Job -Job $job
}
The -JobName parameter is your friend here. Naming jobs makes Get-Job output readable instead of a wall of numeric IDs. When you’re managing 40 concurrent jobs in a production sweep, readable names matter.
If you’re also targeting specific interfaces during data collection — filtering by interface alias before pulling adapter data — the patterns from InterfaceAlias vs InterfaceDescription: Targeting NICs in PowerShell slot in cleanly alongside this workflow.
Error Handling in Async Jobs
Most engineers forget about error handling when they go async. Synchronous scripts fail loudly. Background jobs can fail silently if you’re not checking job state before calling Receive-Job. That’s worse than a visible failure — you’ve burned compute, collected nothing, and have no log entry.
Always check state before harvesting results:
foreach ($job in $jobs) {
if ($job.State -eq 'Failed') {
# Surface the actual failure reason — don't swallow it
Write-Warning "Job $($job.Name) failed: $($job.ChildJobs[0].JobStateInfo.Reason)"
} elseif ($job.State -eq 'Completed') {
Receive-Job -Job $job
}
# Always clean up regardless of outcome
Remove-Job -Job $job
}
A failed job you never inspected is the async equivalent of -ErrorAction SilentlyContinue on a critical operation. Check state. Always.
Trade-offs and Limitations
No tool ships without cost. Here’s where -AsJob creates problems if you’re not deliberate about it.
Session scope. Jobs live in the current PowerShell session. Close the session, lose the jobs and their output. For automation that needs to survive session disconnects or system reboots, look at Register-ScheduledJob or export results to disk before the session ends.
Native support isn’t universal. Common cmdlets like Test-NetConnection do not support -AsJob directly. You’ll need to wrap them in Start-Job or Invoke-Command -AsJob. Test this before building a production pipeline around any specific cmdlet.
Uncapped concurrency creates its own incident. Firing 500 parallel jobs against a slow network segment is not optimization — it’s a self-inflicted denial of service. Throttle concurrency. For network operations across managed environments, 10 to 20 simultaneous jobs per management host is a reasonable starting point.
# Process in controlled batches to avoid hammering the network
$batchSize = 10
for ($i = 0; $i -lt $servers.Count; $i += $batchSize) {
$batch = $servers[$i..([Math]::Min($i + $batchSize - 1, $servers.Count - 1))]
$batchJobs = $batch | ForEach-Object {
Test-Connection -ComputerName $_ -Count 1 -AsJob
}
Wait-Job -Job $batchJobs | Out-Null
$batchJobs | Receive-Job
$batchJobs | Remove-Job
}
This batching pattern keeps blast radius contained. If something goes wrong in batch three, you’ve still got clean output from batches one and two.
For environments where network adapter configuration is part of your automation scope, the performance tuning patterns in Get-NetAdapterRsc: Configure Receive Segment Coalescing in PowerShell pair well here. And if you’re operating in hardened environments with IPsec policies in play, see IPsec Main Mode Crypto Sets: PowerShell Hardening Guide for the encryption side of that picture.
Red Hat’s documentation on parallel task management in Linux environments is worth reading for contrast — the same async-first philosophy applies across platforms, and seeing how it’s implemented in bash job control sharpens your intuition for the PowerShell equivalent.
The Practical Takeaway
-AsJob is not a silver bullet. It is the right tool when your scripts block on network latency, remote execution timeouts, or WMI query time. The pattern is always the same: launch jobs, do other work or wait explicitly, collect results, clean up.
Measure before and after. If your network sweep drops from 20 minutes to 90 seconds, that’s not just a performance win — it’s toil elimination. Over 52 weeks that compounds into real engineering hours recovered. SRE book readers know the math: time saved at scale is error budget preserved.
If you’re building async automation into Windows environments and want a team that runs these patterns in production daily across managed client infrastructure, reach out to SSE. We can audit what you have and replace the blocking scripts.


