Skip to content
LatchkeyLatchkey home
Documentation menu

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.

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 pickup in a few seconds and one in about ten, and it explains most of what this page covers.

01Job queuedGitHub queues a job targeting a latchkey-* label
02WebhookGitHub notifies Latchkey the moment the job queues
03Scale-up decisionA warm runner takes the job, or a fresh machine launches
04PickupWarm: a few seconds. Cold: about 10 seconds, registered just-in-time
05Run and destroyThe job runs alone on the machine, which is terminated afterward
Seconds
warm pickup
when a warm runner is available
~10s
cold start
fresh machine, just-in-time registration
1
job per VM
every runner is destroyed after its job
4h
job cap
the machine is terminated at 4 hours

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 within a few 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 is running your steps in about 10 seconds. Both paths end identically: the runner takes exactly one job and the machine is terminated when the job finishes.

Warm pools#

PlanWarm pool
DeveloperOne warm latchkey-small runner plus parked capacity
LaunchThe same baseline: one warm runner plus parked capacity
ScaleThe 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 in seconds. Anything beyond that, and any job asking for a larger size, cold-starts in about 10 seconds. 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.

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.

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.

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.

Why is my job sitting in queued?

Either it is waiting for capacity beyond the warm baseline, in which case a fresh machine cold-starts in about ten seconds, 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 a few seconds. A cold start provisions a new machine with just-in-time registration and takes about ten seconds. 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.

References