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

StateMeaning
RunningJob is actively executing
CompletedJob finished successfully
FailedJob encountered an error
StoppedJob was manually stopped
BlockedJob is waiting for user input
SuspendedJob 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-Job again returns nothing unless -Keep is used
  • Use -Keep to 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

FeatureStart-JobForEach-Object -Parallel (PS7)
Process isolationFull separate processThread within same process
Startup overheadHigher (full process)Lower (thread)
Variable access-ArgumentList or $Using:$Using: prefix
Available inAll PowerShell versionsPowerShell 7+ only
Best forLong, independent tasksShort, 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.

Leave a Comment