# Background Jobs and Parallel Work

Start-ThreadJob runs a script block in the background while you keep working, Get-Job shows its State, and Receive-Job hands over what it printed; ForEach-Object -Parallel does each item in its own runspace, -ThrottleLimit 4 at a time, which resizes 500 photos in a quarter of the time, with $using: to bring in a variable from outside.

Freelancer · PowerShell · Operating System · Updated November 10, 2026

## After this lesson you can

- Run a script block in the background with Start-ThreadJob, check it with Get-Job, and collect its output with Receive-Job
- Process many items at once with ForEach-Object -Parallel and -ThrottleLimit, bringing in outside variables with $using:

## Key takeaways

- Start-ThreadJob { ... } runs a script block in the background and gives the prompt back at once. Get-Job shows each job's State: Running, Completed or Failed.
- Receive-Job hands over what a job printed, once. -Wait waits for it to finish first, and -AutoRemoveJob tidies the finished job away.
- ForEach-Object -Parallel -ThrottleLimit 4 works on four items at a time. Their lines come back in the order they finish, not the order they went in.
- A -Parallel block can't see your variables: write $using:name to bring one in.
- Parallel work pays off when each item takes a while and doesn't share a file with the others. For tiny jobs, starting the extra workers costs more than it saves.

Your hiking club needs the 500 photos from its summer trip on its website, each one made smaller first. A photo resizer takes a moment for every photo, and a moment times 500 is minutes. Run it the usual way and your PowerShell window is stuck for the whole time, while your computer, which has several processor cores, keeps all but one of them idle. Two fixes: send the work to the background and keep using your window, or do several photos at once. Administrators use the same two fixes to check a hundred PCs or copy a thousand files, and developers to download or test many things at once.

## The stand-in, one at a time

Resizing for real needs an image tool installed. This lesson uses a stand-in that pretends: like a real resizer it takes a moment, longer for some photos than others, then it puts a copy in the `small` folder and says which photo it did. Make 500 empty pretend photos, named the way a camera names them, and the stand-in:

```powershell
New-Item -ItemType Directory -Force -Path "$HOME\terminal-practice\photos\small" | Out-Null
Set-Location "$HOME\terminal-practice\photos"
1..500 | ForEach-Object { New-Item -ItemType File -Force -Name "IMG_$(1000 + $_).jpg" } | Out-Null
Set-Content -Path resize-photo.ps1 -Value '# resize-photo.ps1: a stand-in for a photo resizer. Like a real one, it takes a moment,', '# longer for some photos than others. It puts a copy in the small folder and says which photo it did.', 'param([string] $Path)', 'Start-Sleep -Milliseconds (Get-Random -Minimum 150 -Maximum 350)', 'Copy-Item -Path $Path -Destination "$PSScriptRoot\small"', '"Resized $(Split-Path -Path $Path -Leaf)"'
.\resize-photo.ps1 -Path .\IMG_1001.jpg
```

```text
Resized IMG_1001.jpg
```

How long do 20 photos take, one after another? `Measure-Command` runs the script block in braces and times it:

```powershell
(Measure-Command { Get-ChildItem -Filter *.jpg | Select-Object -First 20 | ForEach-Object { .\resize-photo.ps1 -Path $_.FullName } }).TotalSeconds
```

```text
5.7694268
```

About a quarter of a second each, so all 500 would hold your window for two and a half minutes. Your times will differ a little; the stand-in picks a random time for each photo.

## Into the background: Start-ThreadJob

A **background job** is work PowerShell does out of sight while the prompt stays yours, like dropping a bag at the dry cleaner's and picking it up later. `Start-ThreadJob` starts one from a script block. `-Name` gives it a name to ask for it by:

```powershell
$job = Start-ThreadJob -Name resize -ScriptBlock { Get-ChildItem -Path "$HOME\terminal-practice\photos" -Filter *.jpg | ForEach-Object { & "$HOME\terminal-practice\photos\resize-photo.ps1" -Path $_.FullName } }
Get-Job
```

```text
Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
1      resize          ThreadJob       Running       False           PowerShell            Get-Chil.
```

The prompt is back at once. The job runs in a **runspace** of its own, a separate PowerShell inside the one you're using. It starts in this window's folder, but it can't see this window's variables. **State** says how it's going: Running, then Completed, or Failed. **HasMoreData** turns True once it has printed something you haven't collected. `Receive-Job` collects it; here only the last two lines, after ten seconds:

```powershell
Receive-Job -Name resize | Select-Object -Last 2
```

```text
Resized IMG_1036.jpg
Resized IMG_1037.jpg
```

Thirty-seven done, and the job carries on. `Receive-Job` hands each line over only once, so ask again later for the lines since. When you're ready to wait for the rest, add **`-Wait`**, and **`-AutoRemoveJob`** to tidy the job away once it's done:

```powershell
Receive-Job -Name resize -Wait -AutoRemoveJob | Select-Object -Last 2
```

```text
Resized IMG_1499.jpg
Resized IMG_1500.jpg
```

`Get-Job` now prints nothing: no jobs left. An error inside a job doesn't show up on your screen; it waits, like everything else, until you `Receive-Job`. `Stop-Job` stops a job you no longer want, and `Remove-Job` clears away a finished one.

## Four at a time: ForEach-Object -Parallel

The job freed your window, but the photos still went one at a time. `ForEach-Object -Parallel` runs its script block for several items at once, each in its own runspace. **`-ThrottleLimit`** says how many at a time; 4 is a sensible start on most laptops, and 5 is what you get if you leave it out. Because each runspace starts empty, your variables don't reach it unless you say so: **`$using:`** in front of a name (read "using the value of") brings one in:

```powershell
$resizer = "$HOME\terminal-practice\photos\resize-photo.ps1"
Get-ChildItem -Filter *.jpg | Select-Object -First 8 | ForEach-Object -Parallel { & $using:resizer -Path $_.FullName } -ThrottleLimit 4
```

```text
Resized IMG_1004.jpg
Resized IMG_1002.jpg
Resized IMG_1003.jpg
Resized IMG_1001.jpg
Resized IMG_1007.jpg
Resized IMG_1005.jpg
Resized IMG_1008.jpg
Resized IMG_1006.jpg
```

Each line comes back as its photo finishes, so the order is mixed; yours will be mixed differently. Leave out `using:` and the block gets an empty `$resizer`: the error says the expression after `&` *produced an object that was not valid*. Now the same 20 photos as before, timed:

```powershell
(Measure-Command { Get-ChildItem -Filter *.jpg | Select-Object -First 20 | ForEach-Object -Parallel { & $using:resizer -Path $_.FullName } -ThrottleLimit 4 }).TotalSeconds
```

```text
1.4704839
```

From 5.8 seconds to 1.5: about four times faster, because four photos were in progress at every moment.

![Two ways to resize 20 photos: on top, one lane where the photos go through one after another, labeled One at a time: 5.8 seconds; below, four lanes side by side, each with photos going through at the same time, labeled Four at a time, -ThrottleLimit 4: 1.5 seconds](https://fsdtrainingmedia.blob.core.windows.net/public/images/2026/11/efac835b836a492fb6cabdb9a1b5d923.webp)

All 500 photos took 34 seconds this way, instead of two and a half minutes. Add `-AsJob` to the end, and the whole parallel run becomes one background job, so you get both: four at a time, and your prompt back.

## Which one, when

| You want | Use |
|---|---|
| your prompt back while one long task runs | `Start-ThreadJob`, then `Receive-Job -Wait` |
| many separate items done sooner, and you'll wait | `ForEach-Object -Parallel -ThrottleLimit 4` |
| both | `ForEach-Object -Parallel ... -AsJob` |

Parallel pays off when each item takes a while, waiting for a disk, a network or a tool, and no two items write to the same file. When each item is tiny, starting the extra runspaces costs more than it saves, and a plain `ForEach-Object` wins. You'll also meet `Start-Job`, which starts a whole new PowerShell process for each job: slower to start, but kept fully apart from your window.

`Start-ThreadJob` and `-Parallel` work the same on a Mac or Linux. Windows PowerShell 5.1 has neither built in: it has `Start-Job`, and a plain `ForEach-Object`.

## Check yourself

2 questions to try on the lesson page.

## So what

When work is slow, you don't have to sit and watch it: a background job gives your window back, and `-Parallel` puts the computer's other cores to work, so 500 photos take seconds instead of minutes.

## Key terms

- [Background Job](https://www.ask-codey.com/glossary/background-job-powershell)
- [Start-ThreadJob](https://www.ask-codey.com/glossary/start-threadjob)
- [Receive-Job](https://www.ask-codey.com/glossary/receive-job)
- [ForEach-Object -Parallel](https://www.ask-codey.com/glossary/foreach-object-parallel)
- [Runspace](https://www.ask-codey.com/glossary/runspace)

## Cheat sheets

- [Background Jobs and Parallel Work](https://www.ask-codey.com/cheat-sheets/powershell-jobs-cheat-sheet)

## Related

- [What's Running: \`Get-Process\` and \`Stop-Process\`](https://www.ask-codey.com/lessons/powershell-processes)
- [Starting Programs: \`Start-Process\`](https://www.ask-codey.com/lessons/powershell-start-process)
- [Doing Something to Each: ForEach-Object](https://www.ask-codey.com/lessons/powershell-foreach-object)
- [Counting and Grouping: Measure-Object and Group-Object](https://www.ask-codey.com/lessons/powershell-measure-and-group)

Source: https://www.ask-codey.com/lessons/powershell-jobs
