Skip to content
Back to Quality Assurance

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 set

Example

# .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: 4

Common 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.