PowerShell Jobs: Run Scripts in the Background

Sequential PowerShell loops that ping servers one at a time, copy files one folder at a time, or process records one by one leave performance on the table. PowerShell background jobs let you start multiple operations simultaneously, freeing your console session while work proceeds in the background. This post covers starting jobs with Start-Job, checking status with Get-Job, retrieving results with Receive-Job, waiting for completion, and the parallel jobs pattern for bulk operations.
Start a Background Job
Start-Job runs a script block in a separate PowerShell process and returns a job object immediately, without waiting for the work to finish:
# Start a long-running operation in the background
$job = Start-Job -ScriptBlock {
Get-ChildItem -Path 'C:\Logs' -Recurse -Filter '*.log' |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) }
}
Write-Host "Job started with ID: $($job.Id)"
Write-Host "Continuing other work while job runs..."
Job started with ID: 3
Continuing other work while job runs...
The job runs in its own PowerShell process, which means it has its own scope, its own modules to import, and no access to variables in your current session unless you pass them explicitly.
Check Job Status with Get-Job
Get-Job lists all jobs in the current session with their current state. States include Running, Completed, Failed, and Stopped:
Get-Job | Select-Object Id, Name, State, HasMoreData, PSBeginTime
# Check a specific job
Get-Job -Id 3
# Check if the job is done
if ((Get-Job -Id 3).State -eq 'Completed') {
Write-Host "Job 3 finished"
}
Id Name State HasMoreData PSBeginTime
-- ---- ----- ----------- -----------
3 Job3 Completed True 5/4/2026 09:01:22 AM
Retrieve Results with Receive-Job
Receive-Job returns the output from a completed (or still-running) job. Important: by default Receive-Job removes the output from the job’s buffer — if you call it twice, the second call returns nothing. Always save the results to a variable:
$oldLogs = Receive-Job -Id 3
Write-Host "Found $($oldLogs.Count) log files older than 30 days"
$oldLogs | Select-Object Name, LastWriteTime, Length | Format-Table -AutoSize
To leave data in the buffer (for debugging), use -Keep:
$results = Receive-Job -Id 3 -Keep
Wait for Job Completion
Wait-Job blocks the current session until the specified job (or all jobs) finishes. Combine it with a timeout to prevent waiting indefinitely:
$job = Start-Job -ScriptBlock { Start-Sleep 10; "Done" }
# Block until done (with 30 second timeout)
$completed = Wait-Job -Job $job -Timeout 30
if ($completed) {
$result = Receive-Job -Job $job
Write-Host "Result: $result"
} else {
Write-Warning "Job did not complete within timeout"
Stop-Job -Job $job
}
Remove Completed Jobs
Completed jobs remain in memory until you remove them or close the session. Clean up with Remove-Job to avoid accumulating stale job objects:
# Remove a specific job after collecting results
Receive-Job -Id 3 | Export-Csv "C:\Reports\old-logs.csv" -NoTypeInformation
Remove-Job -Id 3
# Remove all completed jobs at once
Get-Job | Where-Object State -in 'Completed','Failed','Stopped' | Remove-Job
Parallel Jobs Pattern
The classic parallel jobs pattern starts one job per server, waits for all to complete, then collects results. This runs all servers simultaneously instead of one at a time:
$servers = Get-Content "C:\Scripts\servers.txt"
$jobs = @()
foreach ($server in $servers) {
$jobs += Start-Job -ScriptBlock {
param($name)
$ping = Test-Connection -ComputerName $name -Count 1 -Quiet
[PSCustomObject]@{
Server = $name
Online = $ping
Checked = Get-Date
}
} -ArgumentList $server
}
Write-Host "Started $($jobs.Count) jobs. Waiting for completion..."
$jobs | Wait-Job | Out-Null
$results = $jobs | Receive-Job
$jobs | Remove-Job
$results | Sort-Object Server | Format-Table -AutoSize
Server Online Checked
------ ------ -------
Server01 True 5/4/2026 09:15:33 AM
Server02 False 5/4/2026 09:15:34 AM
Server03 True 5/4/2026 09:15:33 AM
Common Errors and Fixes
-
Variables not automatically available in job scope — use ArgumentList. The job script block runs in a separate process with no access to your session’s variables. Pass values explicitly with
-ArgumentListand declare matchingparam()inside the script block. Using$using:scope modifier (available in PS 3+) is an alternative:Start-Job { $using:myVar }. -
Results only retrieved once — save to variable from Receive-Job. Calling
Receive-Jobdrains the output buffer by default. If you call it a second time on the same job, you get nothing. Always capture output:$results = Receive-Job -Job $job.
Related Cmdlets / See Also
Wrapping Up
Background jobs are the PowerShell 5-compatible way to run parallel operations. Start one job per target, wait for them all with Wait-Job, collect results with Receive-Job into a variable, then clean up with Remove-Job. For PowerShell 7, ForEach-Object -Parallel is a cleaner alternative for simple parallel iterations.


