Merge Queues for Serialized Integration
Use a merge queue to serialize PR merges so each PR is validated against the result of the previous merge, preventing the last-merge-fails cascade that breaks main.
Why
When multiple PRs are open and each passes CI against an older main, the first PR merges cleanly — but the second PR was validated against the old main, not the new main, and may break when merged (a semantic conflict that Git cannot detect). Without a merge queue, the team hits the 'last PR fails after merge' problem repeatedly: every PR is green, but main is red after the merge, and someone has to revert or fix forward. A merge queue solves this by queuing PRs and merging them one at a time, re-running the required checks against the result of each previous merge before allowing the next. This trades a small amount of merge latency (each PR waits for the one ahead) for a guarantee that main is always green after every merge. For teams with high PR throughput (more than 10 merges per day), a merge queue is the difference between a stable main and a perpetually broken one.
When
When the repository has more than ~10 PR merges per day, or when the team repeatedly experiences 'main is red after a merge even though all PRs were green' — both indicate that parallel PR validation is no longer sufficient.
How
1. Enable the merge queue (GitHub)
- Settings > General > Pull Requests > Merge queue > Enable
- Or via branch protection: Require merge queue for main
2. Configure the queue
- Maximum entries to build: 5 (batch up to 5 PRs to re-validate together)
- Minimum entries to merge: 1 (merge as soon as one entry passes)
- Maximum concurrent builds: 4 (run 4 queue entries in parallel)
3. Define which checks run in the queue
- The merge queue re-runs the required status checks against the queued base
- Use a conditional in the workflow to run (or skip) checks in the queue context:
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
4. Tune the merge method
- Use squash merge for a linear history (each PR = one commit on main)
- The queue squashes and re-runs checks before merging
5. Monitor queue depth and latency
- Watch the queue length (GitHub shows pending entries)
- If the queue grows past ~20, the CI is too slow — parallelize tests or reduce the required check setExample
# .github/workflows/ci.yml — run in both PR and merge-queue contexts
name: ci
on:
pull_request:
merge_group:
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
# Branch protection: require merge queue
# Settings > Branches > main > Require merge queue
# - Max entries to build: 5
# - Max concurrent builds: 4Common Problems
- ⚠Enabling the merge queue but not adding the merge_group trigger to the workflow — the checks do not run in the queue context, the queue merges without validation, and main breaks exactly as before.
- ⚠Setting the maximum entries to build too high (e.g. 20) — the queue batches 20 PRs, one fails, and all 20 are kicked back to the queue, creating a frustrating feedback loop for the team.
- ⚠Not monitoring the queue depth — the queue silently grows to 50 entries during a busy afternoon, each PR waits an hour to merge, and the team disables the queue to unblock themselves.
- ⚠Using the merge queue with a very slow e2e check (30 minutes) — each queue entry waits 30 minutes, the queue backs up, and the latency outweighs the correctness benefit.
Best Practices
- ✓Add the merge_group trigger to every workflow that runs required checks — without it the queue merges unvalidated, defeating the entire purpose.
- ✓Keep the batch size small (3-5 entries) so a single failure only kicks back a few PRs, not the whole queue — this keeps the feedback loop tight.
- ✓Move slow checks (e2e, full integration) out of the merge queue and into a post-merge or nightly job — the queue should only run fast, reliable checks.
- ✓Set up a queue-depth alert (GitHub API or a Slack bot) that pings the team when the queue exceeds 10 entries, so CI slowness is caught before it forces the queue to be disabled.