Dual-queue model · Label routing · Concurrency · Merge-lane observability
When you run self-hosted runners across multiple rented remote Macs, a common mistake is assuming Merge Queue also schedules machine capacity: GitHub's merge queue governs ordering and safety gates into the default branch, but runner backlog is still shaped by labels and concurrency groups. This article separates the two queues with a control-plane split table, recommends ci-pr / ci-merge / ci-release routing, spells out concurrency and cancel-in-progress rules, and gives criteria for adding Mac capacity versus tuning GitHub first using p95 queuing; it links to our posts on the shared build pool, seat mutex locks, and Mac Mesh task orchestration.
Merge Queue protects default-branch integrity: it sequences batches of checks before integration; it does not guarantee spare CPU on your remote Mac runners. When PR checks and merge checks share the same runs-on labels, GitHub may advance merges while runners are busy with optional PR noise.
Dual-queue confusion: Treating Actions \"Queued\" as if Merge Queue always boosts merge job priority.
Label mixing: A vague ci label piles nightly, PR, and merge traffic onto the same runners.
Bad cancellations: Using the same cancel-in-progress: true policy on merge lanes that you use for PRs, shredding batches mid-flight.
Concurrency collisions: Multiple repos or workflows reuse the same concurrency string and cancel each other.
Weak observability: Not separating merge-queue wait from runner busy time, so capacity debates lack data.
Mesh lease drift: Merge lanes need stable node occupancy but you skipped alignment with lease and handoff thresholds.
Tip: If you are still comparing laptop vs remote roles rather than CI control planes, read local-edit vs remote-build thresholds first.
Use the matrix below in review meetings; doing both is a reasonable default only if runner capacity is honest, not label theater.
| Control plane | GitHub Merge Queue | Runner backlog (machine queue) |
|---|---|---|
| Owns | Order into the default branch, merge batches, required-check semantics | Which Mac runs which workflow job when |
| Does not own | How many performance cores are free on your M4 | Whether a PR should logically cut the line |
| Typical failure | Batches time out despite correct logic but under-powered runners | Merge jobs stay Queued while the queue depth looks short |
| First mitigation | Tighten required checks and batch assumptions | Split runs-on labels, add runners or reserved seats |
Smooth merges depend on runners honestly reserving capacity for merge, not on the Merge Queue toggle alone.
self-hosted + macOS + ci-pr + toolchain pin (for example xcode-16-2).ci-merge served by at least one remote Mac runner.ci-release separate from merge so giant release jobs do not starve trunk.These steps pair with the shared-pool SSH guide: that post answers whether you can reach the runner; this one answers who owns which CPU timeline.
Inventory required checks: List what truly blocks merges on the default branch, duration estimates, and what can move earlier to PR.
Split runs-on: Give merge-only workflows or jobs ci-merge; one physical Mac can advertise many labels, but you must reserve time windows or use dedicated nodes.
Audit concurrency: PR flows use cancel-in-progress: true to shed stale commits; merge and release lanes use cancel-in-progress: false or narrower group keys.
Name concurrency groups: Include repository, workflow, and environment suffixes to avoid org-wide accidents.
Instrument: On the team dashboard separate GitHub merge-queue wait from runner busy minutes.
Gate review: If short queue but long merge-job queue happens twice in a week, change runner topology before debating GitHub parallelism.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
merge-gate:
runs-on: [self-hosted, macOS, ci-merge, xcode-16-2]
timeout-minutes: 45
steps:
- run: echo "merge lane only"
The patterns below come from common postmortems; replace thresholds with your data. The point is never plotting both waits on the same axis.
| Signal | Likely root cause | First move |
|---|---|---|
| Long merge queue, idle runners | Required checks or batching policy too strict | Review checks graph and parallel assumptions |
| Short queue, merge job long-Queued | Label routing or too few runners | Split ci-merge; add nodes or dedicated seats |
| Batches fail as infra | Timeouts, disk, keychain, or network jitter | Check runner logs against the signing governance checklist |
| Fast PR checks, slow merges | PR and merge fight for the same labels | Split runs-on and trim optional PR jobs |
Note: Faster merge hardware without label separation often defers starvation until release night; long term you still need distinct capacity names.
The bands below are typical multi-site program review anchors; wire your SQL or API fields into the internal runbook.
ci-merge, if busy minutes are above 85% in business hours, move nightly to the ci-pr pool.concurrency shared with PR policy.| Team size | Trunk merge cadence | Recommended first move |
|---|---|---|
| Small | Multiple merges per day | One dedicated ci-merge runner + strict required checks |
| Mid-size | Continuous batching | Multi-region runners + explicit release pool |
| Platform | Multi-repo shared pool | Org-wide label standards + cost board splitting queue vs runner |
Using a notebook as a temporary runner invites sleep, thermals, and unaudited interactive login; colo Macs add procurement and cabling. Neither cleanly backs a reviewable merge SLO contract.
For remote Mac pools where iOS CI/CD and long AI-agent jobs coexist, VpsMesh Mac Mini cloud rentals are usually the better fit: scale a runner fleet by region and SKU, place merge, release, and daily noise on named nodes, and put availability and seat policy in ops terms, not Slack folklore.
Not strictly a physical dedicated box, but you must honestly split labels and concurrency; otherwise PR noise starves merge validation. Safer to bind ci-merge to fixed seats or dedicated nodes; scaling paths are on the pricing page and order page.
PR pushes naturally obsolete checks; repeatedly canceling merge batches grows the risk of merged changes without a closed automation loop. Encode policy in YAML and link team runbooks from the help center to reduce misconfiguration.
Start with shared build pool to close SSH and runner registration, then use this article to split merge lanes; seat-lock details are in the mutex TTL article.