Performance

Queue Time

Queue time is the wait between a GitHub Actions job entering the queue and a runner starting it, measured apart from how long the job then takes to run.

Queue time is the interval between the moment a GitHub Actions job enters the queue and the moment a runner claims it and starts the first step. It measures the wait for a machine, and it is a separate interval from run time, which covers how long the steps take once a machine is working on them.

Both intervals land on the same pull request check as wall clock, which is why they get blamed for each other. A team that shortens its test suite and sees no change in the time to a green check was usually waiting for a machine rather than running slow steps.

Definition

The workflow jobs REST API provides started_at and completed_at. It does not provide a general job created_at field from which queue time can be computed. A workflow run's creation time is not a substitute, because jobs may become eligible at different times.

IntervalMeasurementMeaning
Queue timeRecorded queue-entry time to execution startWaiting for an eligible execution environment
Run timecompleted_at minus started_atTime spent executing the job
End-to-end latencyExplicitly chosen event to completionCan include dependencies and approval delays

Queue time is a property of one job rather than one run. A run made of twelve matrix legs has twelve queue times, and the run finishes when the slowest chain of queue plus run finishes. Reporting a single number per run therefore averages away the leg that waited.

Four things live inside a queue time, and they have different fixes:

  1. Every eligible machine is busy. The job waits for one to free up. This is a depth problem in the set of runners that carry the label.
  2. No machine exists yet. An autoscaled fleet creates one on demand, so the wait includes boot, agent registration, and image pull before the first step runs.
  3. A ceiling is holding the job back. Account concurrency limits, a concurrency group in the workflow, or an unfinished needs dependency all keep a job out of a runner while nothing is wrong with capacity.
  4. No machine will ever match. A misspelled label, a repository outside a runner group, or a workflow restriction leaves the job queued with an empty log. GitHub cancels a job that has waited 24 hours for a self-hosted runner (GitHub Actions limits, checked on 2026-08-13), so this failure mode surfaces a day after the push.

Cause three is worth separating in any measurement. A job blocked by needs or by a concurrency group is reported as waiting, so subtracting timestamps across every job in a run mixes dependency waiting into machine waiting. The workflow_job webhook avoids the ambiguity: it delivers queued when the job enters the queue, in_progress when a runner starts it, and completed when it ends, and each payload carries the job's labels array and runner_name (workflow_job webhook payload, checked on 2026-08-13). Recording those deliveries can estimate both intervals with the routing key attached. Deduplicate deliveries, account for retries and delivery delay, and do not treat webhook receipt time as an exact server timestamp.

Read the result as percentiles. Queue time has a long tail by nature, since most jobs on a quiet afternoon start immediately and the jobs that queue arrive together during a busy hour. A mean across a week reports a healthy number while the P90 sits in minutes.

Example

For an existing WarpBuild deployment, use the Jobs and queue timings reports to compare queue and execution duration. If collecting your own data, store queued and started observations per job and run attempt before computing percentiles.

The following conceptual calculation assumes queued_observed_at and started_observed_at are timestamps your own event collector recorded; these are not GitHub REST response fields:

estimated_queue_seconds = started_observed_at - queued_observed_at
run_seconds = completed_at - started_at

An illustrative report has one row per label. The numbers below illustrate the shape of a week from one repository rather than a measurement:

Runner labelJobsWait P90Run P90
ubuntu-latest3558s5m 02s
self-hosted,linux,x644126m 40s4m 10s
self-hosted,linux,arm64961m 12s9m 30s

Three labels, three different problems. The hosted label starts almost immediately and spends its time in the steps. The x64 self-hosted label waits longer than it runs, which points at how many machines carry that label and how quickly new ones appear. The ARM64 label starts quickly and runs for nine minutes, so adding machines there changes nothing and the steps are the place to look.

A single average across all three labels would report roughly four minutes of wait and hide the row that matters. Grouping by label keeps the routing key attached to the number, which is also the key a fix is applied to, since capacity, scope, and machine size are all configured per label.

FAQ

What is queue time in GitHub Actions?

Queue time is the interval between a job entering the queue and a runner claiming it and starting the first step. It measures the wait for a machine. Run time, the interval from the first step to the job conclusion, measures the work itself. A job that shows Waiting for a runner to pick up this job is accumulating queue time.

How do I measure queue time for a job?

Use a documented queue-time report or record workflow_job queued and in_progress events. The workflow-jobs REST response provides started_at and completed_at but should not be assumed to provide a job-created timestamp. Webhook receipt timing is approximate and requires deduplication.

Why report queue time per runner label instead of one average?

The two intervals have different fixes and one average hides which one is growing. A label whose wait exceeds its run time points at capacity or routing. A label whose run time dominates points at the steps. Reporting P75 and P90 per label keeps the two apart, because a long tail on a small share of jobs disappears in a mean.

Sources

Definition reviewed .

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.

← Return to glossary