The measurement
During the v0.59 merge wave (8 PRs in flight) CI reached a hard stall:
2026-08-26T02:20Z completed=34 in_progress=0 queued=401
oldest queued job waiting ~52 min; two samples 20 min apart both showed
in_progress=0 with completed unchanged at 34.
Meanwhile the org's self-hosted fleet was 12 runners online, every one
busy=false.
That is not a misconfiguration — I checked, and the labels match:
| requested by workflows |
runners satisfying it |
[self-hosted, linux, x64, rust-cpu] |
7 of 12 |
[self-hosted, linux, x64, light] |
1 of 12 |
The fleet is schedulable. It is simply almost never asked for:
runs-on: ubuntu-latest 56 job definitions
runs-on: [self-hosted, linux, x64, rust-cpu] 3
runs-on: [self-hosted, linux, x64, light] 2
runs-on: macos-latest 1
56 of 63 job definitions target GitHub-hosted runners. synth runs ~50 jobs
per PR (the oracle sweep is the bulk of it), so N PRs in flight queue ~50N jobs
against GitHub-hosted concurrency shared with the rest of the org — gale and loom
were queued at the same moment — while 12 paid runners idle.
Why it matters beyond waiting
The hub model this repo uses (parallel lanes, PR-not-merge, coordinator gates)
structurally produces bursts of 6–8 simultaneous PRs. At ~50 jobs each that is
a 300–400 job burst every wave. The stall is therefore not a one-off; it is the
expected steady state of the working model, and it currently gates every merge
and therefore every release cut.
What I did NOT do, and why
I did not move jobs to the self-hosted fleet. Under this repo's branch
protection a required check that cannot run deadlocks all merges — so
retargeting any of the 9 required contexts (Format, Clippy, Test,
Z3 Verification, Claim Check, Version Pin Sweep, Bazel Build & Proofs,
Kani Verification, Rivet Validation) is a change to gate infrastructure and
should be deliberate, not a side effect of clearing a queue.
I did cancel three superseded runs (older SHAs on branches that had been
force-pushed, including a deliberate red-first probe whose RED result is already
recorded in #1060's body). Verified surgical: current-head rollups unchanged and
zero required checks left CANCELLED on any of the 8 open PRs.
Options, roughly in increasing order of commitment
- Move the non-required oracle sweep to
rust-cpu. The ~40 scripts/repro/*
execution differentials are the bulk of the burst and none of them are in the
required set, so a mis-scheduled one blocks nothing. Largest relief, lowest
risk.
- Stagger lane pushes so a wave lands as 2–3 PRs rather than 8. Cheap, but
it slows the hub and does not use the idle capacity.
- Retarget some required checks to self-hosted. Highest relief, highest
risk — needs the deadlock caveat above respected, and probably a period of
running both before flipping the required context.
- More GitHub-hosted concurrency. Spend, not engineering.
Option 1 looks like the right first move: it is reversible, it touches nothing
required, and it aims exactly at the jobs that make the burst large.
Reproduce
gh api orgs/pulseengine/actions/runners --jq '.runners[]|"\(.name) \(.labels|map(.name)|join(","))"'
grep -hoE '^\s+runs-on:.*' .github/workflows/*.yml | sed 's/^\s*runs-on:\s*//' | sort | uniq -c | sort -rn
Note for whoever measures the queue: gh run list --json status reports run
status, and a run stays queued while its jobs complete. Census at job level
(actions/runs/<id>/jobs) or the number is misleading.
The measurement
During the v0.59 merge wave (8 PRs in flight) CI reached a hard stall:
Meanwhile the org's self-hosted fleet was 12 runners online, every one
busy=false.That is not a misconfiguration — I checked, and the labels match:
[self-hosted, linux, x64, rust-cpu][self-hosted, linux, x64, light]The fleet is schedulable. It is simply almost never asked for:
56 of 63 job definitions target GitHub-hosted runners. synth runs ~50 jobs
per PR (the oracle sweep is the bulk of it), so N PRs in flight queue ~50N jobs
against GitHub-hosted concurrency shared with the rest of the org — gale and loom
were queued at the same moment — while 12 paid runners idle.
Why it matters beyond waiting
The hub model this repo uses (parallel lanes, PR-not-merge, coordinator gates)
structurally produces bursts of 6–8 simultaneous PRs. At ~50 jobs each that is
a 300–400 job burst every wave. The stall is therefore not a one-off; it is the
expected steady state of the working model, and it currently gates every merge
and therefore every release cut.
What I did NOT do, and why
I did not move jobs to the self-hosted fleet. Under this repo's branch
protection a required check that cannot run deadlocks all merges — so
retargeting any of the 9 required contexts (
Format,Clippy,Test,Z3 Verification,Claim Check,Version Pin Sweep,Bazel Build & Proofs,Kani Verification,Rivet Validation) is a change to gate infrastructure andshould be deliberate, not a side effect of clearing a queue.
I did cancel three superseded runs (older SHAs on branches that had been
force-pushed, including a deliberate red-first probe whose RED result is already
recorded in #1060's body). Verified surgical: current-head rollups unchanged and
zero required checks left
CANCELLEDon any of the 8 open PRs.Options, roughly in increasing order of commitment
rust-cpu. The ~40scripts/repro/*execution differentials are the bulk of the burst and none of them are in the
required set, so a mis-scheduled one blocks nothing. Largest relief, lowest
risk.
it slows the hub and does not use the idle capacity.
risk — needs the deadlock caveat above respected, and probably a period of
running both before flipping the required context.
Option 1 looks like the right first move: it is reversible, it touches nothing
required, and it aims exactly at the jobs that make the burst large.
Reproduce
Note for whoever measures the queue:
gh run list --json statusreports runstatus, and a run stays
queuedwhile its jobs complete. Census at job level(
actions/runs/<id>/jobs) or the number is misleading.