# How provisioning works

> What happens between a job queueing on GitHub and a runner picking it up: warm pools, cold starts with just-in-time registration, the concurrency ceiling, and why a job might wait.

Source: https://latchkey.dev/documentation/runner-provisioning

## Summary

- A queued job is picked up by a warm runner that is already online in about two seconds, or a fresh machine cold-starts with just-in-time registration.
- Every runner is ephemeral: one job per VM, single-use credentials, machine terminated after the job.
- Every plan gets the same warm baseline today: one always-on warm runner plus parked capacity on `latchkey-small`. Demand beyond it cold-starts.
- 20 concurrently busy runners per workspace by default; idle warm runners never count against the limit.

When GitHub queues a job with a `latchkey-*` label, a webhook tells Latchkey immediately, and Latchkey makes one decision: hand the job to a runner that is already warm, or launch a fresh machine for it. You never see that decision, but it is the difference between a job that starts almost at once and one that waits for a machine to boot, and it explains most of what this page covers.

## Warm pickup vs cold start

A **warm pickup** means a pre-provisioned runner was already online for your workspace: the job is handed straight to it and starts in about two seconds. A **cold start** means no warm runner fit, so a fresh machine boots just for that job, registers with GitHub using a just-in-time, single-use configuration, and then runs your steps. Both paths end identically: the runner takes exactly one job and the machine is terminated when the job finishes.

## Warm pools

| Plan | Warm pool |
| --- | --- |
| Developer | One warm `latchkey-small` runner plus parked capacity |
| Launch | The same baseline: one warm runner plus parked capacity |
| Scale | The same baseline: one warm runner plus parked capacity |

Every plan gets the same warm baseline today, and it covers `latchkey-small` only: one always-on warm runner backed by parked machines that resume on demand. Anything beyond that, and any job asking for a larger size, cold-starts. Warm capacity costs you nothing while it idles; billing is per job minute, and an idle warm runner is not running a job.

## Ephemeral, always

Provisioning never reuses a machine. Warm or cold, a runner takes exactly one job and is terminated afterward, along with its disk, which is why runner names in the GitHub run view change on every run and why anything a job writes to local disk is gone when the job ends. The security side of this design, just-in-time credentials, private networking, and encrypted single-use disks, is covered in [Security architecture](/documentation/security-architecture).

## Concurrency

A workspace runs up to **20 concurrently busy runners** by default, and only busy runners count: idle warm runners never consume a slot, so warm capacity does not compete with your real jobs. When a burst needs more than 20 at once, the overflow jobs stay queued until a slot frees, then start on their own; nothing errors and nothing is lost. If your peaks queue regularly, higher limits are available, [contact support](/support).

## Hard boundaries

- Jobs are capped at **4 hours**; the machine is terminated at 4 hours even if the job is still running.
- Runners are **Ubuntu 24.04 LTS on x86_64** only: no Windows, macOS, arm64, or GPU hosts.
- Runners run in **AWS us-east-1**.

The full table, including disk sizes and per-plan custom configuration counts, is in [Limits and concurrency](/documentation/runner-limits).

## Why a job might sit in queued

When provisioning cannot or will not launch a runner, there is no error on GitHub's side; the job just waits. That is by design: jobs targeting `latchkey-*` labels stay queued on GitHub rather than erroring. The usual causes:

- A **label typo**: no configuration matches the label, so nothing ever picks the job up.
- The **repository is not monitored**, or the runner **configuration is disabled**.
- A **billing or trial block**: an expired trial, a lapsed subscription, or an exhausted free tier on a card-less trial. A "Managed runner blocked" notification fires when this happens, at most once per day.
- A **custom runner's image is still building**; jobs targeting its label wait until the build completes.
- The workspace is at its **20-busy-runner ceiling**; the job starts as soon as a slot frees.

The symptom-by-symptom walkthrough, with how to confirm each cause, is in [Troubleshooting](/documentation/troubleshooting).

### Why is my job sitting in queued?

Either it is waiting for capacity beyond the warm baseline, in which case a fresh machine is cold-starting for it, or something is blocking it: the label, monitoring, configuration, billing, an in-progress image build, or the concurrency ceiling. Check them in that order.

### What is the difference between a warm pickup and a cold start?

A warm pickup hands the job to a machine that is already running, in about two seconds when the runner is already online. A cold start provisions a new machine with just-in-time registration and takes longer. Every plan gets the same warm baseline today: one always-on warm runner plus parked capacity on `latchkey-small`.

### Do idle warm runners count against my concurrency limit?

No. The default of 20 concurrent runners per workspace counts busy runners only, so warm capacity waiting for work never consumes the limit you are paying attention to.

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
