A merge queue tests each pull request against the target branch plus everything ahead of it in the queue, then merges in order. It exists to solve one specific problem: two pull requests that pass on their own and break main when combined.
This is a semantic conflict, and git will not catch it because there is no textual conflict to catch. Someone deletes a helper function in one branch; someone else adds a call to it in another. Both branches are green. Both merge cleanly. Main is broken, and the person who notices is whoever pulls next.
Requiring branches to be up to date before merging solves it and creates a different problem — on a busy branch, every merge invalidates everyone else’s status checks, so the whole team spends the day updating and re-running CI. The queue is the way out of that trade.
Table of contents
- How the queue actually validates
- The one-line change that makes CI actually run
- Settings that matter, and the ones that do not
- When you do not need one
- How this fits the rest of the stack
- FAQ
How the queue actually validates
When a pull request enters the queue, GitHub creates a temporary branch containing the target branch, every pull request already queued ahead of it, and then this one. CI runs against that combination.
So with three queued pull requests, three temporary branches exist:
main+ PR 1main+ PR 1 + PR 2main+ PR 1 + PR 2 + PR 3
Each one is tested. If all pass, they merge in order and main was never broken at any point. This speculative execution is what makes the queue fast — it does not wait for one to finish before starting the next.
When something fails, the queue removes the offending pull request and rebuilds the branches behind it without those changes. The author is notified, and everyone else’s work carries on rather than being stuck behind a broken change.
The result is a branch that is green by construction rather than by convention. Teams that adopt this generally report that a whole category of post-merge build failure disappears, which also removes the on-call interruption that used to come with it.
The one-line change that makes CI actually run
This is the part that catches everyone, because the failure is quiet. Workflows triggered only on pull_request do not fire for merge-queue validation, because a merge group is a different event. Your required checks never run, and depending on configuration you either block forever or merge with no validation at all.
on:
pull_request:
merge_group:
That second line is the whole fix. Add it to every workflow that produces a required status check.
Then be deliberate about what runs where. The queue is the last gate before main, so it should run the slow, thorough suite. The pull request itself should run the fast feedback loop.
name: CI
on:
pull_request:
merge_group:
jobs:
fast:
# Lint and unit tests -- runs on both events
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm test
integration:
# Slow suite -- only when actually merging
if: github.event_name == 'merge_group'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:integration
That split is where the real value shows up on a large repository: contributors get quick feedback, and the expensive suite runs once per merge rather than on every push to every branch.
Settings that matter, and the ones that do not
Enable it through branch protection on the target branch — require a merge queue — and then set the behaviour.
- Merge method. Squash keeps main linear and readable, which pairs well with a queue since the queue is already imposing an order.
- Maximum pull requests to build concurrently. This is the speculation depth. Higher means faster throughput and more CI minutes burned on branches that get rebuilt when something ahead fails. On a flaky suite, high concurrency is expensive.
- Minimum and maximum group size. Merging in batches is cheaper in CI time and makes failures harder to attribute, since a failed group has to be bisected. Batching is worth it when CI is slow and the suite is reliable.
- Status check timeout. Set it above your slowest realistic run. Too low and healthy pull requests get evicted for taking a normal amount of time.
- Only merge non-failing pull requests. Leave this on. It is the point of the feature.
The setting that quietly determines whether the whole thing works is none of these — it is whether your test suite is reliable. A queue amplifies flakiness ruthlessly, because a flaky failure in a speculative branch evicts a pull request and rebuilds everything behind it. On a suite with a two percent flake rate and deep speculation, the queue spends a meaningful fraction of its time undoing itself.
Fix the flakes before enabling the queue, not after. Otherwise the queue gets blamed for a problem it merely made visible.
When you do not need one
Merge queues are designed for busy branches with many people merging daily. Below that, they add latency and ceremony for a problem you do not have.
A team of four merging a handful of pull requests a day will essentially never hit a semantic conflict, and when they do, someone notices within minutes because everyone is looking at the same code. Requiring branches to be up to date before merging is sufficient and simpler.
The signals that you have crossed the threshold:
- Main breaks after a merge more than about once a week, from combinations rather than from individual mistakes.
- People routinely update their branch and re-run CI purely because someone else merged first.
- There is an informal convention about who merges when, or a chat message asking people to hold off.
- A post-merge break interrupts on-call.
The third one is the clearest tell. When a team has invented a manual queue in a chat channel, the tool is just formalising what is already happening.
The alternative worth considering first is making CI faster. A five-minute pipeline makes the update-and-rerun cycle tolerable and reduces the pressure that creates the problem. A forty-minute pipeline makes every process question harder, and a queue on top of a slow pipeline is a slow queue.
How this fits the rest of the stack
A merge queue protects the branch. What it does not tell you is whether the merged result works in production, which is a different question answered by a different environment — and the gap between green CI and a working deploy is where most incidents actually live.
RunxBuild closes that gap on the deploy side. A web service or static site builds from your GitHub repository on merge, with the build log and the runtime logs in the same place, so a release that compiles and then fails on startup is visible immediately rather than inferred from a support ticket. A bad deploy rolls back to the previous one. Managed MySQL and Postgres sit beside the service on private networking, and autoscaling moves between a floor and ceiling plan you choose. To see what a service, a database and its storage add up to per environment, the RunxBuild hosting calculator lists them as separate line items.
Useful related references:
- Undoing a Merge in Git: Revert, Reset, and the Trap That Follows
- Git Revert on a Merge: Why It Needs -m and What It Breaks Later
- The n8n Merge Node: Combining Two Streams Without Losing Items
- Deploying from GitHub on RunxBuild
FAQ
What problem does a GitHub merge queue solve?
Two pull requests that each pass CI on their own but break the branch when combined — a semantic conflict, like one branch deleting a function another branch starts calling. Git sees no textual conflict, so both merge cleanly and main breaks. The queue tests each change against everything ahead of it before merging.
Why does my CI not run in the merge queue?
Because a merge group is a separate event from a pull request. A workflow triggered only on pull_request never fires for queue validation, so required checks either block forever or are skipped entirely. Add merge_group: to the on: block of every workflow that produces a required check.
Should I run all my tests in the merge queue?
Run the fast suite on both the pull request and the queue, and gate the slow integration suite on the merge-group event only. Contributors get quick feedback, and the expensive suite runs once per merge instead of on every push. Conditioning a job on the event name is enough to split them.
Do I need a merge queue for a small team?
Probably not. Below several merges a day, semantic conflicts are rare and quickly noticed, and requiring branches to be up to date before merging is simpler. The clearest signal that you need one is a team that has already invented an informal queue in a chat channel.
What happens if a queued pull request fails CI?
It is removed from the queue and its author is notified. The temporary branches behind it are rebuilt without those changes, so the pull requests after it keep moving instead of being stuck. This is also why a flaky test suite is expensive here: a spurious failure evicts a change and forces rebuilds of everything queued behind it.