Skip to content
compiler.dev

GitHub Merge Queue: Setup, Workflows and Pitfalls

When many people merge into main all day, a PR that passed CI against yesterday's main can break today's. A merge queue fixes that: it tests each PR combined with the PRs ahead of it, and merges only if that combined result passes. This guide covers how it works, how to set it up, and the CI changes it needs.

The problem it solves

Branch protection can require PRs to be "up to date" before merging. That is safe, but it forces everyone to update their branch and rerun CI each time someone else merges, a race that gets worse with team size. Without that rule, two PRs that each pass alone can break main together, for example two changes to the same function signature in different files.

How the queue works

  1. A developer clicks "Merge when ready" (or adds the PR to the queue).
  2. GitHub creates a temporary branch containing the base branch, the PRs ahead in the queue, and this PR. Branch names look like gh-readonly-queue/main/pr-123-<sha>.
  3. It triggers your workflows with the merge_group event on that branch.
  4. If required checks pass, the PR is merged. If they fail, the PR is removed from the queue and the PRs behind it are rebuilt without it.

Developers still run PR checks as before. The queue adds a second, final check on the combined result.

Availability: GitHub's docs say merge queues are available for public repositories owned by an organization, and for private repositories on GitHub Enterprise Cloud. Check the current plan requirements in the docs before you plan around it.

Step 1: Make workflows run on merge_group

This is the step most teams miss. Required status checks in a queue are only reported if the workflow triggers on merge_group. If it does not, the queued PR waits for checks that never run.

on:
  pull_request:
  merge_group:
    types: [checks_requested]

Every workflow that provides a required check needs this trigger, including the lightweight ones such as lint or a tests-passed summary job. Be careful with paths filters: if a required check is skipped by a path filter, the queue still waits for that check to report, so test this on a low-traffic branch first.

Also check actions/checkout. In a merge_group run, github.sha is the merge commit of the temporary branch, so the default checkout gets the right code. Avoid steps that assume github.event.pull_request exists; it does not for merge_group. For diff-based logic, such as affected builds, compare against github.event.merge_group.base_sha.

Step 2: Enable it in a ruleset or branch protection

In repository settings under Rules (rulesets) or Branches (branch protection), enable "Require merge queue" for your default branch, and choose these options:

SettingWhat it doesTypical choice
Merge methodMerge, squash or rebase for queued PRsSquash, to match your history
Build concurrencyHow many queue entries are tested at once3 to 5
Minimum group sizeSmallest batch to merge together1
Maximum group sizeLargest batch merged in one go3 to 5 for busy repos
Wait time to meet minimumHow long to wait for a group to fill5 minutes, or less
Status check timeoutFail a queue entry if checks take too longAbout twice your CI time
Only merge non-failing PRsWhether a failing PR in a group blocks othersEnable

Defaults are conservative. Build concurrency controls throughput: with 5 concurrent entries and a 10-minute CI, the queue can merge much faster than one at a time, because each entry is tested speculatively on top of the earlier ones.

Step 3: Required checks that match

Add the checks you want enforced on the queue branch as required status checks in the same ruleset. Use stable names; a matrix generates names that change when the matrix changes. A single summary job fixes that; see matrix builds done right.

Decide which checks to run twice. The usual pattern: run fast checks (lint, unit tests) on both PR and queue; run slow checks (full end-to-end, multi-OS) only in the queue, so every PR does not pay for them. That makes the queue the place where cost is concentrated, and keeps PR feedback quick.

What it does to cost and speed

Merge queues add runs: each queued PR runs CI at least once more. With speculative builds, a failure in an earlier entry cancels and restarts later ones. Estimates:

  • Extra minutes per merged PR is about one full CI run of the checks you require in the queue.
  • If failures are frequent, restarts multiply that. Flaky tests are the main cause; see dealing with flaky tests.
  • Larger groups (batching) cut the number of runs: with max group size 5, five PRs can share one CI run, at the price of harder diagnosis when it fails.

Model it with the CI minutes forecaster: PRs per day x queue CI minutes x rate. Then weigh it against the cost of a broken main, which usually blocks everyone.

Pitfalls

  • Checks never start: missing merge_group trigger.
  • Skipped required checks: a job skipped by if: can report as skipped, which counts as passing for required checks in some configurations. Use the summary-job pattern with if: always().
  • Secrets and permissions: merge_group runs in the context of the repository, with normal access; review which secrets those workflows touch.
  • Bots and dependency updates: Dependabot PRs work with the queue; enable auto-merge on them rather than merging by hand. See Dependabot setup.
  • Required reviews and CODEOWNERS still apply before a PR can enter the queue. See CODEOWNERS and branch protection.
  • Emergency fixes: admins can bypass or jump the queue depending on your ruleset; decide the policy beforehand.

Alternatives

If you cannot use GitHub's merge queue, third-party tools provide similar behaviour: Mergify, Aviator, Graphite's queue and Bors-style bots. They work by creating the same kind of temporary combined branch. The concerns above, trigger setup and cost, apply to all of them.

FAQ

Do I need to enable the queue to use auto-merge?

No. Auto-merge merges when requirements are met. With a queue enabled, it adds the PR to the queue instead.

Does the queue rebase my PR?

It merges the PR onto the temporary branch; the final method follows your merge method setting. Your PR branch is not updated.

What if checks pass on the PR but fail in the queue?

The PR is removed from the queue. That means the combination with other changes broke something, which is what the queue is for. Update the branch, fix and re-queue.

Does it work with required deployments?

Environments can be part of required checks, but test this on a low-traffic branch before relying on it.

Measure the effect

Track time from "ready to merge" to merged, and the number of broken-main incidents before and after. When queue CI is the bottleneck, a faster runner shortens everyone's wait: compiler.dev's comparison mode lets you test your queue workflow on faster machines and compare cost per run.

Made by compiler.dev. Free tools · Pricing