PowerShell Jobs: Run Scripts in the Background

PowerShell Jobs: Run Scripts in the Background

PowerShell Tips Editor 3 min read
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 -ArgumentList and declare matching param() 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-Job drains 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.

Send-Item -To