GitHub Merge Queues: Serializing main Without Parking Every PR
A protected main with "require branches to be up to date" turns every popular repo into a rebase lottery. Two approved PRs pass CI; the first merges; the second is suddenly stale; its author rebases; CI runs again; someone else merged in the meantime. Merge queues exist to make that race a server-side problem: PRs line up, GitHub builds a temporary integration branch that already contains everything ahead in the queue, and only then fast-forwards main.
What actually happens when you click Merge
With a queue enabled on a branch rule, Merge does not merge. It enqueues. GitHub creates a branch that is main plus PR A plus PR B (in queue order), runs the checks you require on that combination, and if they pass, merges A, then repeats for B on the new main. If B's combined build fails, B is kicked out and A can still land. The authors of A and B did not rebase by hand.
This is the same idea as GitLab's merge trains and Google's internal submit queues. The invariant is: nothing reaches main that has not been tested with the commits that will precede it.
CI has to be queue-aware
If your workflows key off pull_request only, the queue's merge_group events will not run them, and GitHub will wait forever or skip the guarantee. You need:
on:
pull_request:
merge_group:
Treat merge_group like a PR against the integration ref. Use the same required checks the branch rule names — the names must match exactly, including workflow job names. A check that only runs on pull_request and is marked required will block the queue in a way that is miserable to debug: the PR looks green, the queue looks stuck.
Keep the queue CI fast. Every extra minute is multiplied by queue depth. Split a 25-minute suite: required-on-queue gets lint, unit, and a smoke e2e; nightly gets the rest. If the required set is the full matrix, you have built a serial bottleneck with extra steps.
Failure policy is the product
When a grouped build fails, the default is to drop the PR that made it fail and re-test the rest. That is correct for a flaky-unrelated failure caused by this PR, and brutal if CI is flaky on its own. A 2% flake rate in a queue of ten is not a 2% delay; it is reshuffles. Invest in quarantine and retries before you turn the queue on, or you will train the team to bypass it.
Do not put "merge when ready" on PRs that still need a human to watch a deploy. The queue is a correctness tool for the default branch, not a replacement for environment approvals. Keep production deploys on main with a separate protection or environment gate.
When the queue is the wrong tool
Small repos with one or two merges a day should keep "up to date" plus a rebase. The queue adds concepts (group branches, extra workflows, admin UI) for a race that barely happens.
Monorepos with path-filtered CI need extra care: the merge group contains other people's files. A path filter that skipped tests on the PR may not skip them on the group — or worse, may skip a test that the other PR needed. Prefer "run the union of affected packages" over naive path filters when queued.
If your tests are not hermetic (they hit a shared staging database), the queue will serialize load onto that database and fail in new ways. Fix isolation first.
A sane rollout
Enable the queue on a non-main integration branch for a week, or on main with a small required check set. Watch queue wait time and kick-out rate. Tell reviewers that Approve + Merge now means "enter the train," not "it is on main." Update bots: Dependabot and Renovate should enqueue like humans, and they should not open a follow-up rebase PR that fights the queue.
The payoff is cultural as much as technical. Rebase-to-merge was unpaid work that punished whoever landed second. A merge queue makes main a single writer with tested prefixes. That is how you stop treating green PRs as a suggestion.
Keep reading
Designing a Metrics System: Time-Series Storage from Gorilla to Downsampling
Ten million series, one datapoint each per 10 seconds, queried by tags: delta-of-delta compression, the inverted index over labels, and why high cardinality kills TSDBs.
Designing Petabyte Log Search: Index Everything vs Grep Smarter
Logs are 100x your metrics volume and queried 0.001% as often. The Splunk-style full index, the Loki-style label-only bet, and bloom-filtered brute force in between.
Designing a Distributed Tracing Backend: Dapper-Style Sampling and Storage
Tracing every request would need a system bigger than the one being traced. Head vs tail sampling, span ingestion pipelines, and storage laid out for trace reads.
CI/CD Pipelines for Data Engineers
Data pipelines are production software. Here's how to build CI/CD that catches bad transforms before they corrupt dashboards: testing strategy, environment promotion, slim runs, and rollback patterns.
Feature Flags: How to Stop Coupling Deployment to Release
Shipping code and releasing a feature are different events that most teams accidentally fuse together. Feature flags split them — and unlock trunk-based development, safe rollouts, and instant rollback.
OpenTelemetry in .NET: Distributed Tracing Without Vendor Lock-In
Instrument your .NET services once with OpenTelemetry and ship traces to Jaeger, Grafana Tempo, or Azure Monitor — all without changing application code.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.