PowerShell Background Jobs
A background job runs a PowerShell command or script in a separate process — independently from the main console. Instead of waiting for a long-running task to finish before typing the next command, background jobs free the console immediately while work continues behind the scenes. This is essential for parallel processing, long-running maintenance tasks, and non-blocking automation.
Why Use Background Jobs?
Without Jobs: With Jobs:
---------------------- ----------------------
Start-Sleep -Seconds 60 Start-Job { Start-Sleep -Seconds 60 }
[console frozen for 60 sec] [console available immediately]
Only one task at a time Multiple tasks run in parallel
Starting a Background Job
# Start a simple background job
$job = Start-Job -ScriptBlock {
Start-Sleep -Seconds 5
Write-Output "Job completed at $(Get-Date)"
}
Write-Host "Job started. ID: $($job.Id) | Status: $($job.State)"
Write-Host "Console is free to use while job runs..."
Output:
Job started. ID: 1 | Status: Running
Console is free to use while job runs...
Managing Jobs
Get-Job – View All Jobs
Get-Job
Output:
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
1 Job1 Completed True localhost Start-Sleep...
2 Job2 Running False localhost Get-Process...
Job States
| State | Meaning |
|---|---|
| Running | Job is actively executing |
| Completed | Job finished successfully |
| Failed | Job encountered an error |
| Stopped | Job was manually stopped |
| Blocked | Job is waiting for user input |
| Suspended | Job was paused (Workflow jobs) |
Receiving Job Results
# Start a job that returns data
$job = Start-Job -ScriptBlock {
Get-Process | Sort-Object CPU -Descending | Select-Object -First 3
}
# Wait for job to complete
Wait-Job -Job $job
# Retrieve the results
$result = Receive-Job -Job $job
$result | Format-Table Name, CPU
Output:
Name CPU
---- ---
chrome 18.45
code 9.10
svchost 3.20
Receive-Job Behavior
- Results are consumed once collected — calling
Receive-Jobagain returns nothing unless-Keepis used - Use
-Keepto preserve results for re-reading
$result = Receive-Job -Job $job -Keep # Results stay available
Wait-Job – Wait for a Job to Finish
$job = Start-Job -ScriptBlock { Start-Sleep -Seconds 10; "Done" }
Write-Host "Waiting for job..."
Wait-Job -Job $job
Write-Host "Job finished: $(Receive-Job -Job $job)"
# Wait with a timeout (seconds)
$completed = Wait-Job -Job $job -Timeout 15
if ($completed -eq $null) {
Write-Host "Job timed out!"
}
Stop and Remove Jobs
# Stop a running job
Stop-Job -Job $job
# Remove a completed job from memory
Remove-Job -Job $job
# Remove all completed jobs
Get-Job | Where-Object { $_.State -eq "Completed" } | Remove-Job
# Remove all jobs
Get-Job | Remove-Job -Force
Passing Arguments to Jobs
Variables from the current scope are not automatically available inside a job's script block — jobs run in isolated processes. Use -ArgumentList to pass values.
$targetPath = "C:\Logs"
$maxFiles = 100
$job = Start-Job -ScriptBlock {
param ($path, $limit)
Get-ChildItem -Path $path | Select-Object -First $limit
} -ArgumentList $targetPath, $maxFiles
Wait-Job $job
Receive-Job $job
Running Multiple Parallel Jobs
$servers = @("Server01", "Server02", "Server03", "Server04")
# Start a job for each server simultaneously
$jobs = foreach ($server in $servers) {
Start-Job -ScriptBlock {
param ($srv)
$reachable = Test-Connection -ComputerName $srv -Count 1 -Quiet
[PSCustomObject]@{
Server = $srv
Online = $reachable
}
} -ArgumentList $server
}
# Wait for all jobs to complete
$jobs | Wait-Job | Out-Null
# Collect all results
$results = $jobs | Receive-Job
$results | Format-Table -AutoSize
# Clean up
$jobs | Remove-Job
Output:
Server Online
------ ------
Server01 True
Server02 True
Server03 False
Server04 True
Start-Job vs ForEach-Object -Parallel
| Feature | Start-Job | ForEach-Object -Parallel (PS7) |
|---|---|---|
| Process isolation | Full separate process | Thread within same process |
| Startup overhead | Higher (full process) | Lower (thread) |
| Variable access | -ArgumentList or $Using: | $Using: prefix |
| Available in | All PowerShell versions | PowerShell 7+ only |
| Best for | Long, independent tasks | Short, data-intensive iterations |
ForEach-Object -Parallel (PowerShell 7)
$servers = @("Server01", "Server02", "Server03", "Server04", "Server05")
$results = $servers | ForEach-Object -Parallel {
$reachable = Test-Connection -ComputerName $_ -Count 1 -Quiet
[PSCustomObject]@{
Server = $_
Online = $reachable
}
} -ThrottleLimit 5 # Max 5 threads simultaneously
$results | Format-Table
Named Jobs
# Give jobs descriptive names for easy tracking
$job1 = Start-Job -Name "BackupJob" -ScriptBlock { Start-Sleep 10; "Backup done" }
$job2 = Start-Job -Name "ReportJob" -ScriptBlock { Start-Sleep 5; "Report done" }
# Access by name
Get-Job -Name "BackupJob"
Receive-Job -Name "BackupJob"
Remove-Job -Name "ReportJob"
Job Output Streams
$job = Start-Job -ScriptBlock {
Write-Output "Success output"
Write-Error "Error output"
Write-Warning "Warning output"
Write-Verbose "Verbose output" -Verbose
}
Wait-Job $job | Out-Null
# Receive all streams
$job | Receive-Job -ErrorVariable jobErrors -WarningVariable jobWarnings
Write-Host "Errors: $jobErrors"
Write-Host "Warnings: $jobWarnings"
Real-World Example – Parallel Log Archiving
$servers = @("WebServer01", "WebServer02", "AppServer01")
$archiveJobs = foreach ($server in $servers) {
Start-Job -Name "Archive_$server" -ScriptBlock {
param ($srv)
$logPath = "\\$srv\C$\Logs"
$archivePath = "\\$srv\C$\LogArchive"
$cutoff = (Get-Date).AddDays(-7)
if (Test-Path $logPath) {
$files = Get-ChildItem -Path $logPath -Filter "*.log" |
Where-Object { $_.LastWriteTime -lt $cutoff }
foreach ($f in $files) {
Move-Item $f.FullName $archivePath -Force
}
"$srv – Archived $($files.Count) files"
} else {
"$srv – Log path not found"
}
} -ArgumentList $server
}
Write-Host "Archiving started on $($archiveJobs.Count) servers. Working..."
$archiveJobs | Wait-Job | Out-Null
$archiveJobs | ForEach-Object {
Write-Host (Receive-Job $_)
Remove-Job $_
}
Summary
Background jobs run commands in separate processes, freeing the console for other work. Start-Job launches a job. Get-Job monitors status. Wait-Job waits for completion. Receive-Job collects results. Remove-Job cleans up memory. Running multiple jobs in parallel — one per server — turns sequential operations that take minutes into parallel operations that complete in seconds. PowerShell 7's ForEach-Object -Parallel offers a lighter-weight alternative for pipeline-based parallel processing.
