Skip to content
.md

How Merge Queues Work

This page explains the internal mechanics of a merge queue — how PRs flow through it, how test branches are created, and how the queue coordinates with your CI system.

A pull request goes through several stages in a merge queue:

Add to queue

Start CI

CI passes

CI fails

Ready to merge

Success

Merge conflict

Removed from queue

Done

Pending

Testing

Passed

Failed

Merging

Merged

When a PR is added to the merge queue, the queue:

  1. Validates eligibility — Is the PR approved? Are required checks passing?
  2. Assigns position — Based on priority and arrival time
  3. Records the base — Captures the current state of the target branch
Merge QueuePull RequestDeveloperMerge QueuePull RequestDeveloperAdd PR to queueCheck eligibilityAssign position✅ Queued at position

The merge queue creates a temporary branch that represents “what main will look like after this PR merges.” This is the key insight that makes merge queues work.

mainfeaturetestmainABmerge

The test branch contains:

  • All commits from main
  • All commits from PRs ahead in the queue (if using speculative checks)
  • The PR’s changes, merged in

The merge queue triggers CI on the test branch. This is often called “queue CI” to distinguish it from the CI that runs on the PR branch itself.

CI SystemGit ServerMerge QueueCI SystemGit ServerMerge QueuePush test branchTrigger workflowRun tests...Report status

The queue monitors CI status and waits for all required checks to complete.

If CI passes:

Pull Requestmain branchMerge QueuePull Requestmain branchMerge QueueFast-forward or merge commitClose as mergedRemove from queueNotify next PR

If CI fails:

DeveloperPull RequestMerge QueueDeveloperPull RequestMerge QueueRemove from queueAdd failure commentNotify of failureRe-test PRs behind this one

Understanding how the queue manages multiple PRs is crucial.

At any moment, the queue contains PRs in various states:

Queue State:
┌─────────────────────────────────────────────┐
│ Position │ PR │ Status │ Base │
├─────────────────────────────────────────────┤
│ 1 │ #101 │ Testing │ abc123 │
│ 2 │ #102 │ Testing │ +#101 │
│ 3 │ #103 │ Pending │ +#102 │
│ 4 │ #104 │ Pending │ +#103 │
└─────────────────────────────────────────────┘

Each PR’s “base” includes all PRs ahead of it — this is how the queue ensures PRs are tested against the future state of main.

When PR #102 fails, the queue must re-evaluate everything behind it:

After #102 Fails

Before Failure

#101 Testing (base: main)

#102 Testing (base: +#101)

#103 Pending (base: +#102)

#101 Testing (base: main)

#102 ❌ Removed

#103 Re-testing (base: +#101)

PR #103’s test is now invalid — it was tested against a world where #102 existed, but #102 is gone. The queue automatically re-tests #103 with a new base.

When a PR passes CI, the merge queue must integrate it into main. There are several strategies:

Creates a merge commit, preserving the full branch history.

mainfeaturemainABCMerge PR #123

Pros: Full history, easy to revert Cons: Cluttered history with merge commits

Combines all PR commits into a single commit.

mainmainAB+C (squashed)

Pros: Clean linear history Cons: Loses individual commit granularity

Replays PR commits on top of main.

mainmainAB (rebased)C (rebased)

Pros: Linear history, preserves commits Cons: Changes commit hashes

Only possible when the test branch is already based on current main. Moves the main pointer forward.

Pros: No extra commits, preserves exact SHA tested in queue Cons: Not always possible

The merge queue needs tight integration with your CI system.

You configure which CI checks must pass before a PR can merge:

required_checks:
- build
- test
- lint

The queue must be able to:

  1. Trigger CI on the test branch
  2. Receive status updates when checks complete
  3. Cancel CI if a PR is removed from the queue

Push branch

Webhook

Status API

Merge Queue

Git Server

CI System

If CI fails due to a flaky test:

  • Some queues offer automatic retry (1-2 times)
  • Some require manual re-queue
  • Some track flake rates and adjust behavior

See Troubleshooting for more on handling flaky tests.

Test branches are temporary. Here’s their lifecycle:

  1. Created when PR starts testing
  2. Updated if base changes (PR ahead merges/fails)
  3. Used for merge if CI passes
  4. Deleted after merge or failure

Most merge queues clean up test branches automatically. You’ll see branches like:

  • mq/main/pr-123
  • gh-readonly-queue/main/pr-123-abc1234
  • mergify/merge-queue/main/pr-123

What if main changes while CI is running?

1. PR #1 starts testing against main@A
2. Someone pushes directly to main (now main@B)
3. PR #1's CI passes
4. Should PR #1 merge?

Different queues handle this differently:

  • Strict: Require re-test against new main
  • Optimistic: Allow merge if no conflicts
  • Configurable: Based on freshness policy

If the test branch has merge conflicts:

  • The queue cannot create the test branch
  • The PR is removed from the queue
  • Developer must resolve conflicts and re-queue

If someone force-pushes to a PR while it’s in the queue:

  • Most queues detect this and re-start testing
  • Some queues remove the PR and require re-queue

A merge queue works by:

  1. Queuing PRs in order of priority and arrival
  2. Creating test branches that represent the future state of main
  3. Running CI on these test branches
  4. Merging only if CI passes
  5. Re-testing downstream PRs when failures occur

This ensures that every commit on main has been tested against the exact state it will merge into — eliminating the “two green PRs make a red main” problem.

Next, explore whether you need a merge queue or dive into specific features.