PowerShell jobs can also be used beyond the local process-based model described in Execution & Administration: Jobs page. Here we focus on two more advanced approaches: running jobs on remote computers with Invoke-Command -AsJob, and running lightweight, high-performance jobs locally with thread jobs. While standard background jobs manage asynchronous execution on the local computer, enterprise automation often requires distributing background tasks across remote networks or running lightweight concurrent tasks locally without process overhead. PowerShell handles these workloads using remote jobs via Invoke-Command -AsJob and thread jobs via Start-ThreadJob.
Running Remote Commands as Jobs with Invoke-Command -AsJob
The -AsJob parameter on Invoke-Command runs a remote command asynchronously, immediately returning a job object instead of waiting for the remote command to finish. When running commands across multiple remote machines, executing them synchronously can cause scripts to hang if one or more nodes respond slowly. Appending -AsJob converts the execution into a background PSRemotingJob, returning control to the local shell immediately while remote hosts process the task in parallel.
$job = Invoke-Command -ComputerName "SRV01" -ScriptBlock { Get-Process } -AsJobwhere -AsJob runs the remote script block as a background job rather than blocking until completion.
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
3 Job3 Running True SRV01 Get-ProcessRetrieving Remote Job Results
Wait-Job -Job $job
Receive-Job -Job $jobProcessName Id PSComputerName
----------- -- --------------
chrome 4032 SRV01
svchost 1580 SRV01Running a Remote Job Against Multiple Computers
$job = Invoke-Command -ComputerName "SRV01", "SRV02" -ScriptBlock { Get-Service -Name "WinRM" } -AsJob
Wait-Job -Job $job
Receive-Job -Job $jobStatus Name DisplayName PSComputerName
------ ---- ----------- --------------
Running WinRM Windows Remote Management (WS-Manag...) SRV01
Running WinRM Windows Remote Management (WS-Manag...) SRV02Complete Example with Job Name
$servers = @("Server01", "Server02", "Server03")
$job = Invoke-Command -ComputerName $servers -ScriptBlock {
Start-Sleep -Seconds 3
Get-Service -Name "wuauserv" | Select-Object -Property Name, Status, StartType
} -AsJob -JobName "FleetServiceAudit"
$job | Select-Object -Property Id, Name, PSJobTypeName, State, HasMoreDatawhere:
Invoke-Commanddispatches commands across network endpoints using WinRM (or SSH).-ComputerNameis an array of target hostnames.-AsJobinstructs PowerShell to create a background remoting job and immediately return the job object to the console.-JobNameassigns a user-defined identifier to the parent remoting job.PSJobTypeNamereports the runtime engine backing the job (PSRemotingJob).
./invoke-remote-job.ps1Id : 6
Name : FleetServiceAudit
PSJobTypeName : RemoteJob
State : Running
HasMoreData : TrueLocal Jobs vs Remote Jobs
| Feature | Start-Job |
Invoke-Command -AsJob |
|---|---|---|
| Execution location | Local machine (separate process) | Remote machine(s) |
| Requires remoting/WinRM | No | Yes |
Results include PSComputerName |
No | Yes |
| Best for | Local background tasks | Distributing work across servers |
Thread Jobs with Start-ThreadJob
The classic Start-Job runs each job in a separate process, which provides strong isolation but comes with significant memory and startup overhead. Start-ThreadJob (from the ThreadJob module) runs jobs as threads within the current process instead, making it much faster and lighter for running many small, parallel tasks.
Start-ThreadJob -ScriptBlock { Get-Process }Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
4 Job4 Running True PowerShell Get-ProcessClassic Jobs vs Thread Jobs
| Feature | Start-Job |
Start-ThreadJob |
|---|---|---|
| Isolation | Full process isolation | Shared process (thread-based) |
| Startup overhead | Higher | Much lower |
| Memory usage | Higher (per process) | Lower (shared process memory) |
| Best for | Long-running, isolated tasks | Many short, parallel tasks |
| Module required | Built-in | ThreadJob (built-in on PS 7+) |
Running Many Thread Jobs in Parallel
$computers = "SRV01", "SRV02", "SRV03", "SRV04"
$jobs = foreach ($computer in $computers) {
Start-ThreadJob -ScriptBlock {
param($name)
Test-Connection -ComputerName $name -Count 1 -Quiet
} -ArgumentList $computer
}
$jobs | Wait-Job | Receive-Job
$jobs | Remove-Jobwhere:
Start-ThreadJoblaunches a lightweight thread-based job for each computer.-ArgumentListpasses the loop variable into the job's script block.
./parallel-thread-jobs.ps1True
True
False
TrueThread Job Demonstration with Thread IDs
$threadJobs = 1..4 | ForEach-Object {
Start-ThreadJob -Name "Worker_$_" -ScriptBlock {
param($Index)
Start-Sleep -Milliseconds (Get-Random -Minimum 500 -Maximum 1500)
[PSCustomObject]@{
ThreadIndex = $Index
ThreadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId
ExecutionTime = (Get-Date -Format "HH:mm:ss.fff")
}
} -ArgumentList $_
}
$results = $threadJobs | Wait-Job | Receive-Job
$threadJobs | Remove-Job
$results | Format-Table -AutoSizewhere:
Start-ThreadJoblaunches an asynchronous background job as an in-process thread.-Namefriendly label applied to the thread job instance.-ScriptBlockcode statements executed on the background thread.[System.Threading.Thread]::CurrentThread.ManagedThreadIdretrieves the OS managed thread ID, proving each worker executes on a distinct thread within the same host process.-ArgumentListpasses variables into the thread job's isolated parameter scope.
./thread-job-demo.ps1ThreadIndex ThreadId ExecutionTime
----------- -------- -------------
1 13 23:28:25.913
2 15 23:28:26.445
3 18 23:28:26.381
4 20 23:28:25.851Job Architectural Comparison
Selecting the appropriate job type depends on resource constraints, network isolation, and thread safety requirements:
| Feature / Metric | Classic Jobs (Start-Job) |
Thread Jobs (Start-ThreadJob) |
Remote Jobs (Invoke-Command -AsJob) |
|---|---|---|---|
| Execution Host | Local machine | Local machine | Remote target servers |
| Execution Model | Separate OS child process | Separate thread in current process | Remote WinRM / SSH runspace |
| Memory Footprint | Heavy (~30-60 MB per job) | Lightweight (~1-2 MB per thread) | 0 MB locally (hosted on remote RAM) |
| Startup Overhead | High (initializes new shell) | Extremely Low (\10 ms) | Network latency dependent |
| Process Crash Impact | Isolated; child crash leaves parent safe | Fault in native DLL can crash host process | Isolated to remote node |
| Module Availability | Built-in | Included in PS 7+ / PSGallery for PS 5.1 | Built-in (Microsoft.PowerShell.Core) |
Choosing the Right Job Type
| Scenario | Recommended Approach |
|---|---|
| Run one script in the background locally | Start-Job |
| Run a command on a remote server | Invoke-Command (synchronous) |
| Run a command on many remote servers without blocking | Invoke-Command -AsJob |
| Run many small, fast, local parallel tasks | Start-ThreadJob |
| Need full process isolation for a risky/unstable script | Start-Job |
Practical Usage
Parallel Remote Health Checks with Thread Jobs
Combining Start-ThreadJob with Invoke-Command allows checking many remote servers in parallel without the overhead of full background job processes for each.
$servers = "SRV01", "SRV02", "SRV03"
$jobs = foreach ($server in $servers) {
Start-ThreadJob -ScriptBlock {
param($computer)
Invoke-Command -ComputerName $computer -ScriptBlock {
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
WinRMStatus = (Get-Service WinRM).Status
Uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
}
}
} -ArgumentList $server
}
$jobs | Wait-Job | Receive-Job | Format-Table -AutoSize
$jobs | Remove-Jobwhere each thread job wraps its own Invoke-Command call, distributing both the local thread overhead and the remote execution across servers simultaneously.
./parallel-remote-healthcheck.ps1ComputerName WinRMStatus Uptime PSComputerName
------------ ----------- ------ --------------
SRV01 Running 2.14:22:10 SRV01
SRV02 Running 0.08:03:45 SRV02
SRV03 Running 5.01:17:52 SRV03Orchestrating Asynchronous Fleet Operations
In automated patching or health checks, administrators must dispatch remote actions across a server farm, track progress asynchronously without blocking other management tasks, and harvest results once all nodes finish.
$servers = @("Server01", "Server02", "Server03")
Write-Output "Dispatching background audit across $($servers.Count) nodes..."
$fleetJob = Invoke-Command -ComputerName $servers -ScriptBlock {
$uptime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
$freeSpace = (Get-Volume -DriveLetter 'C').SizeRemaining / 1GB
[PSCustomObject]@{
BootTime = $uptime
CFreeGB = [math]::Round($freeSpace, 2)
}
} -AsJob -JobName "FleetHealthScan"
while ($fleetJob.State -eq "Running") {
Write-Output "Waiting for remote systems to complete... (Current State: $($fleetJob.State))"
Start-Sleep -Seconds 2
}
$report = Receive-Job -Job $fleetJob -Keep
Remove-Job -Job $fleetJob
$report | Select-Object -Property PSComputerName, BootTime, CFreeGB |
Format-Table -AutoSizewhere:
while ($fleetJob.State -eq "Running")polls the parent job's state property non-blockingly, allowing the parent script to display progress or run other workloads while waiting.Receive-Job -Keepfetches results from all child jobs while keeping the cached data in memory for auditing.Remove-Jobdeletes both the parent container and all child jobs, freeing connection resources and memory.PSComputerNameis an injected remoting metadata property showing which server produced each metric.
./orchestrate-fleet-audit.ps1Dispatching background audit across 3 nodes...
Waiting for remote systems to complete... (Current State: Running)
Waiting for remote systems to complete... (Current State: Running)
PSComputerName BootTime CFreeGB
-------------- -------- -------
Server01 3/15/2026 08:30:12 AM 45.28
Server02 3/20/2026 02:15:40 AM 18.90
Server03 3/28/2026 11:00:22 PM 82.14