Skip to content

PowerShell Remoting: Background and Thread Jobs

How to run remote PowerShell commands asynchronously with Invoke-Command -AsJob and execute lightweight, high-performance thread jobs locally for parallel task automation.

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.

Consolepowershell
$job = Invoke-Command -ComputerName "SRV01" -ScriptBlock { Get-Process } -AsJob

where -AsJob runs the remote script block as a background job rather than blocking until completion.

Outputplaintext
Id  Name    State    HasMoreData   Location   Command
--  ----    -----    -----------   --------   -------
3   Job3    Running  True          SRV01      Get-Process

Retrieving Remote Job Results

Consolepowershell
Wait-Job -Job $job
Receive-Job -Job $job
Outputplaintext
ProcessName   Id    PSComputerName
-----------   --    --------------
chrome        4032  SRV01
svchost       1580  SRV01

Running a Remote Job Against Multiple Computers

Consolepowershell
$job = Invoke-Command -ComputerName "SRV01", "SRV02" -ScriptBlock { Get-Service -Name "WinRM" } -AsJob
Wait-Job -Job $job
Receive-Job -Job $job
Outputplaintext
Status   Name   DisplayName                              PSComputerName
------   ----   -----------                              --------------
Running  WinRM  Windows Remote Management (WS-Manag...)  SRV01
Running  WinRM  Windows Remote Management (WS-Manag...)  SRV02

Complete Example with Job Name

invoke-remote-job.ps1powershell
$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, HasMoreData

where:

  • Invoke-Command dispatches commands across network endpoints using WinRM (or SSH).
  • -ComputerName is an array of target hostnames.
  • -AsJob instructs PowerShell to create a background remoting job and immediately return the job object to the console.
  • -JobName assigns a user-defined identifier to the parent remoting job.
  • PSJobTypeName reports the runtime engine backing the job (PSRemotingJob).
Consolepowershell
./invoke-remote-job.ps1
Outputplaintext
Id            : 6
Name          : FleetServiceAudit
PSJobTypeName : RemoteJob
State         : Running
HasMoreData   : True

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

Consolepowershell
Start-ThreadJob -ScriptBlock { Get-Process }
Outputplaintext
Id  Name    State    HasMoreData   Location   Command
--  ----    -----    -----------   --------   -------
4   Job4    Running  True          PowerShell Get-Process

Classic 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

parallel-thread-jobs.ps1powershell
$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-Job

where:

  • Start-ThreadJob launches a lightweight thread-based job for each computer.
  • -ArgumentList passes the loop variable into the job's script block.
Consolepowershell
./parallel-thread-jobs.ps1
Outputplaintext
True
True
False
True

Thread Job Demonstration with Thread IDs

thread-job-demo.ps1powershell
$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 -AutoSize

where:

  • Start-ThreadJob launches an asynchronous background job as an in-process thread.
  • -Name friendly label applied to the thread job instance.
  • -ScriptBlock code statements executed on the background thread.
  • [System.Threading.Thread]::CurrentThread.ManagedThreadId retrieves the OS managed thread ID, proving each worker executes on a distinct thread within the same host process.
  • -ArgumentList passes variables into the thread job's isolated parameter scope.
Consolepowershell
./thread-job-demo.ps1
Outputplaintext
ThreadIndex 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.851

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

parallel-remote-healthcheck.ps1powershell
$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-Job

where each thread job wraps its own Invoke-Command call, distributing both the local thread overhead and the remote execution across servers simultaneously.

Consolepowershell
./parallel-remote-healthcheck.ps1
Outputplaintext
ComputerName WinRMStatus Uptime           PSComputerName
------------ ----------- ------           --------------
SRV01        Running     2.14:22:10       SRV01
SRV02        Running     0.08:03:45       SRV02
SRV03        Running     5.01:17:52       SRV03

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

orchestrate-fleet-audit.ps1powershell
$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 -AutoSize

where:

  • 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 -Keep fetches results from all child jobs while keeping the cached data in memory for auditing.
  • Remove-Job deletes both the parent container and all child jobs, freeing connection resources and memory.
  • PSComputerName is an injected remoting metadata property showing which server produced each metric.
Consolepowershell
./orchestrate-fleet-audit.ps1
Outputplaintext
Dispatching 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
Navigation