Chapter 17Lesson 01~200 minutes

Background Jobs and Job Lifecycle

Learn the complete PowerShell background-job lifecycle, including process isolation, serialization, stream collection, bounded concurrency, cancellation, result retrieval, and cleanup.

JobsAsyncLifecycleSerialization

Learning objectives

  • Explain asynchronous work and the PowerShell job object model.
  • Use Start-Job, Get-Job, Wait-Job, Receive-Job, Stop-Job, and Remove-Job safely.
  • Explain why background jobs serialize objects across a process boundary.
  • Inspect job output and non-success streams without losing task identity.
  • Bound process-job creation rather than launching unbounded work.
  • Build and clean up a structured multi-task background-job lab.

1. Why asynchronous work exists

A synchronous command occupies the current pipeline until it finishes. That is desirable when the next step depends on its result, but inconvenient when several independent operations spend most of their time waiting. Asynchronous execution means starting work and regaining control before that work has completed.

A PowerShell job is a managed asynchronous task. It is not merely “another process”: PowerShell tracks its state, streams, child jobs, output availability, and lifecycle through job objects.

Why asynchronous work exists
flowchart TD;
    A["Parent PowerShell session"] --> B["Start-Job"];
    B --> C["Separate pwsh process / session"];
    C --> D["Work"];
    D --> E["Serialized job streams"];
    E --> F["Receive-Job"];
  

2. A job object is a control record, not the result

Start-Job returns immediately with a job object. The job object tells you whether work is Running, Completed, Failed, Stopped, or in another lifecycle state. The actual output is buffered separately until you retrieve it.

$job = Start-Job -Name 'academy-demo' -ScriptBlock {
    Start-Sleep -Milliseconds 400
    [pscustomobject]@{
        Task       = 'inventory'
        ProcessId  = $PID
        FinishedAt = [datetime]::UtcNow
    }
}

$job | Select-Object Id,Name,State,HasMoreData,PSJobTypeName,Location
Get-Job -Id $job.Id | Select-Object Id,Name,State,HasMoreData

The parent prompt can continue while the separate job process runs. That separation is the source of both isolation and overhead.

3. The lifecycle is start, observe, wait, receive, then clean up

Production scripts should treat jobs as resources with a lifecycle. Starting is only the first step. You must eventually consume or record the result and remove the job object.

$job = Get-Job -Name 'academy-demo'
$job | Wait-Job
$result = $job | Receive-Job
$result

$job | Remove-Job
Get-Job -Name 'academy-demo' -ErrorAction SilentlyContinue
Cmdlet Purpose
Start-Job Create local background work in a separate PowerShell process/session.
Get-Job Inspect jobs known to the current parent session.
Wait-Job Block until selected jobs reach a terminating state.
Receive-Job Retrieve aggregated output/stream data produced so far.
Stop-Job Request that a running job stop.
Remove-Job Delete the job object and its stored job data from the current session.

4. Receive-Job consumes aggregated output unless you keep it

By default, receiving a job drains the aggregated stream data that was returned. Current PowerShell documentation provides -Keep when you need to inspect the same aggregate again. This matters during diagnostics: “the second Receive-Job returned nothing” can be normal behavior rather than data loss in the child operation.

$job = Start-Job { 1..3 | ForEach-Object { [pscustomobject]@{ Number=$_; Square=$_*$_ } } }
$job | Wait-Job

$first = $job | Receive-Job -Keep
$second = $job | Receive-Job -Keep

[pscustomobject]@{ FirstCount=@($first).Count; SecondCount=@($second).Count }
$job | Receive-Job | Out-Null   # consume aggregate
$job | Remove-Job

5. Background jobs cross a process boundary

Start-Job runs in a separate process. Values crossing that boundary use PowerShell remoting serialization. Many familiar objects therefore arrive as deserialized snapshots: useful properties remain, but live methods or handles tied to the child process cannot simply be transferred.

$job = Start-Job { Get-Process -Id $PID }
$job | Wait-Job
$p = $job | Receive-Job

$p.PSObject.TypeNames | Select-Object -First 3
$p | Get-Member | Select-Object -First 12
$job | Remove-Job
Design implication: return plain, stable data contracts from background work—IDs, names, durations, status, measurements—rather than expecting a live process/file/network object to survive serialization.

6. Jobs collect PowerShell streams, not only success output

A job can emit Success, Error, Warning, Verbose, Debug, and Information data. The job and its child-job objects expose these collections, while Receive-Job replays aggregated stream data to the caller.

$job = Start-Job {
    Write-Output      ([pscustomobject]@{ Kind='Data'; Value=42 })
    Write-Warning     'training warning'
    Write-Information 'training information'
    Write-Error       'training non-terminating error'
}
$job | Wait-Job | Out-Null

$job.ChildJobs[0] | Select-Object State
$job.ChildJobs[0].Error   | Select-Object FullyQualifiedErrorId,CategoryInfo
$job.ChildJobs[0].Warning | Select-Object Message

$job | Receive-Job -Keep
$job | Remove-Job

7. Start-Job has no ThrottleLimit: bound creation yourself

Unlike Start-ThreadJob and ForEach-Object -Parallel, Start-Job has no built-in -ThrottleLimit. Starting hundreds of process jobs at once can create severe CPU, memory, and process pressure. A basic bounded pattern launches only a small batch at a time.

$tasks = 1..6
$maxConcurrent = 2
$results = [System.Collections.Generic.List[object]]::new()

for ($offset=0; $offset -lt $tasks.Count; $offset += $maxConcurrent) {
    $last = [math]::Min($offset + $maxConcurrent - 1, $tasks.Count - 1)
    $batch = $tasks[$offset..$last]
    $jobs = foreach ($task in $batch) {
        Start-Job -ArgumentList $task -ScriptBlock {
            param($n)
            Start-Sleep -Milliseconds (150 + 20*$n)
            [pscustomobject]@{ Task=$n; Square=$n*$n; WorkerPid=$PID; Success=$true }
        }
    }
    $jobs | Wait-Job | Out-Null
    foreach ($j in $jobs) { $results.Add(($j | Receive-Job)) }
    $jobs | Remove-Job
}
$results | Sort-Object Task

This explicit batching pattern is intentionally simple. The next lessons show higher-level PowerShell primitives with built-in throttling.

8. Stopping a job is a lifecycle action, not rollback

Stop-Job asks PowerShell to stop the background task. It cannot reverse an external side effect that already happened. Cancellation and rollback are different concerns. A job that wrote half a deployment must still have idempotency/checkpoint/recovery design.

$job = Start-Job { Start-Sleep -Seconds 30; 'late result' }
Start-Sleep -Milliseconds 200
$job | Stop-Job
$job | Wait-Job
$job | Select-Object State,HasMoreData
$job | Receive-Job -ErrorAction SilentlyContinue
$job | Remove-Job

9. Lab — run a bounded multi-task inventory and preserve identity

This lab creates only four harmless process jobs and runs at most two at a time. Each worker returns a structured record that includes its task identity, state, duration, and any error text.

$items = 1..4
$maxConcurrent = 2
$records = @()

for ($offset=0; $offset -lt $items.Count; $offset += $maxConcurrent) {
    $slice = $items[$offset..([math]::Min($offset+$maxConcurrent-1,$items.Count-1))]
    $jobs = foreach ($item in $slice) {
        Start-Job -ArgumentList $item -ScriptBlock {
            param($id)
            $sw=[Diagnostics.Stopwatch]::StartNew()
            try {
                Start-Sleep -Milliseconds (100*$id)
                [pscustomobject]@{ Id=$id; Success=$true; DurationMs=$sw.ElapsedMilliseconds; Error=$null }
            } catch {
                [pscustomobject]@{ Id=$id; Success=$false; DurationMs=$sw.ElapsedMilliseconds; Error=$_.Exception.Message }
            }
        }
    }
    $jobs | Wait-Job | Out-Null
    $records += $jobs | Receive-Job
    $jobs | Remove-Job
}
$records | Sort-Object Id
$jobs | ForEach-Object { Get-Job -Id $_.Id -ErrorAction SilentlyContinue }
# No output means the lab jobs were removed.

10. Common job mistakes

Mistake Failure mode Better pattern
Start many process jobs without a bound Process/memory pressure can overwhelm the host. Batch or use an abstraction with a throttle.
Assume the job object contains task output Code reads metadata instead of results. Wait/receive explicitly.
Expect live methods after Start-Job Cross-process serialization changes type fidelity. Return plain structured data.
Forget Remove-Job Long sessions accumulate job objects and buffered data. Cleanup in finally or a controlled lifecycle.

11. Why this matters in DevOps

Jobs are useful when several independent checks, builds, or inventory calls can overlap and process isolation is worth the overhead. They are especially useful as a teaching model because they force you to separate task identity, state, output, errors, cancellation, and cleanup.

12. Verification checklist

  • You can distinguish asynchronous job management from merely starting an unrelated process.
  • You know Start-Job uses a separate process/session and therefore serialization.
  • You can inspect job state and retrieve retained output deliberately.
  • You know how to inspect error/warning streams on child jobs.
  • You avoid unbounded Start-Job creation.
  • You remove jobs after the results have been handled.

13. Knowledge check

Question 1. What does Start-Job return immediately?

Question 2. Why can an object returned from Start-Job lose live methods?

Question 3. What does Receive-Job -Keep change?

Question 4. Does Stop-Job undo external changes already made?

Question 5. Does Start-Job provide a built-in ThrottleLimit parameter?

14. Summary and next bridge

Background jobs provide strong local isolation by running in separate PowerShell processes, but isolation costs startup time and serialization. The next lesson uses threads/runspaces inside the current process: lighter-weight and often faster, but with a much more important shared-memory/thread-safety boundary.

15. Authoritative references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.