Skip to content
Julien Danjou Julien Danjou
June 24, 2026 · 7 min read

When to Outgrow GitHub's Merge Queue

GitHub's native merge queue is the right starting point. Six signals it is time to upgrade, and what each one tells you about what to switch to.

GitHub’s native merge queue is the right starting point for most teams. It costs nothing, it ships with the platform, and it solves the basic “do not break main” problem.

It also has a ceiling.

The teams we talk to who graduate from it usually report the same failure modes. PRs piling up faster than the queue can churn them. Hotfixes stuck behind feature work. The same flaky test failing four times in a row. CI bills creeping up because every PR runs a full test suite, even when ten of those PRs touch the same file.

If you are hitting any of the signals below, you have outgrown what GitHub’s queue is designed to do. None of these are bugs in GitHub’s product. They are deliberate design choices, tuned for teams that are not yet at your scale.

Signal 1: Your queue can’t keep up with your PR volume

GitHub’s queue merges PRs one at a time. Each PR runs against a fresh base built from main plus the PRs ahead of it. A serial queue can only merge as fast as CI runs: with a 30-minute suite that is about two PRs an hour. That is fine at 10 PRs a day. Once your team is merging 20 or 30 a day, the queue cannot finish one iteration before the next wave of PRs arrives, and the backlog grows faster than it drains.

flowchart LR
  subgraph GH["GitHub: one CI run per PR"]
    direction LR
    A1["PR 1"] --> A2["PR 2"] --> A3["PR 3"] --> A4["PR 4"]
  end
  subgraph MG["Mergify: one CI run per batch"]
    direction LR
    B["PR 1 + 2 + 3 + 4"] --> R{"Batch passes?"}
    R -->|"yes"| M["Merge all 4"]
    R -->|"no"| S["Bisect, merge the good ones"]
  end
What you need

Batching. A batched queue tests N PRs as a single unit. If the batch passes, all N merge in one go. CI runs once instead of N times, the queue clears faster, and the bill drops.

The hard part of batching is what happens when the batch fails. Naive batching reruns the whole batch on every failure, which can be worse than no batching. Smart batching splits the batch on failure and bisects to find which PR is responsible. The non-failing PRs merge, the broken one gets kicked back to its author with a real error.

Mergify’s merge queue ships batching with bisect-on-failure by default. The first month most teams see a 60-80% reduction in queue-related CI minutes for their highest-traffic repos.

Signal 2: Hotfixes sit behind feature work

GitHub’s queue treats all PRs as equal. You can use a “jump to front” mechanism to bump one PR ahead, but you cannot set multiple priority lanes or have the queue automatically route PRs to lanes based on rules.

What you need: multi-level priorities tied to PR metadata
priority:hotfix

skips the queue, merges immediately

priority:default

runs normally, in order

priority:low

waits in line behind everything

This sounds cosmetic until you have an active incident and discover your one-shot fix is queued behind 30 feature PRs. The wait time for that fix becomes a measurable production cost.

Signal 3: You cannot afford to run full CI on every PR

A standard CI pipeline runs the same set of jobs on every push to every PR. As your test suite grows past 30 minutes, this becomes expensive. Past an hour, it becomes unworkable.

flowchart TB
  P["Every push"] --> L["Light checks<br/>lint · types · changed-file unit tests"]
  L --> Q{"PR enters queue"}
  Q --> H["Heavy checks<br/>integration · e2e · full matrix"]
  H --> MG["Merge — blessed on the in-queue result"]
What you need

Two-step CI. Light checks on every push (lint, type-check, unit tests for the changed files) and heavy checks (integration tests, end-to-end suites, full matrix runs) only when the PR enters the queue.

GitHub’s queue does not support this. You can fake it by carefully gating your workflows with if: conditions, but you lose the test-on-final-state guarantee that is the entire point of a merge queue.

Mergify’s two-step CI runs explicit pre-queue and in-queue checks. You define what runs where in YAML. The merge queue only blesses the result of the in-queue checks.

Signal 4: Your monorepo runs the wrong tests

If you have a monorepo with five services and a PR touches one service, GitHub’s queue cannot scope the test run to that service. It runs the full pipeline. At monorepo scale, this is a per-PR tax on velocity.

What you need

Scope-aware batching. The queue groups PRs by which scope (service, package, directory) they touch and tests each scope independently. A PR touching service-a/ runs service-a tests. A batch of three PRs all touching service-b/ runs service-b tests once and merges all three.

service-a/runs service-a tests
#4121 CI run
service-b/runs service-b tests
#415#418#421
batched · 1 CI run · merges 3
service-c/runs service-c tests
#4301 CI run

Each scope is an independent lane. A PR is tested only against the scope it touches; PRs sharing a scope batch into one run.

Most engineering orgs that move to monorepos hit this wall within six months. Mergify ships scope-aware batching as part of the queue configuration. You define scopes in your config and the queue does the routing.

Signal 5: Flaky tests are quietly blocking merges

We cover this in depth in our guide to flaky tests. The short version: a merge queue without flaky test handling gets slower every month as your test suite grows.

GitHub’s queue has no concept of flaky tests. A flaky failure looks identical to a real failure. The queue blocks, the engineer reruns, the queue blocks again. You either lower your bar (auto-retry-once-then-ignore, which hides real bugs) or you suffer.

What you need — integrated with the queue, so detection can change queue behavior
Detect

flag tests that fail then pass on the same SHA

Quarantine

stop them from blocking the merge

Smart retry

retry only the flaky one, not the suite

Signal 6: There was a silent merged-without-CI incident

GitHub’s queue rebuilds merge commits server-side after CI passes. The merged SHA is not always the SHA your CI signed off on, because the queue can re-resolve the merge against newer base commits. In rare cases, this means code lands that was not exactly the code that was tested.

flowchart TB
  subgraph GH["GitHub native"]
    direction TB
    g1["CI passes on SHA abc123"] --> g2["Queue rebuilds the merge commit"]
    g2 --> g3["Merged SHA = def456"]
    g3 --> g4{"tested ≠ merged"}
  end
  subgraph MG["Mergify"]
    direction TB
    m1["CI passes on SHA abc123"] --> m2["No rebuild after CI"]
    m2 --> m3["Merged SHA = abc123"]
    m3 --> m4{"tested = merged"}
  end

This is documented and was discussed at length on Hacker News. For most teams it never matters. For teams shipping payment systems, healthcare data, or anything regulated, it is a non-starter.

Mergify does not rebuild the merge commit after CI. The tested SHA is the merged SHA. If that property matters to your audit posture, that alone is reason to switch.

What the migration looks like

For a single repo, migration is a day’s work:

1

Install the Mergify GitHub App on the repo

2

Add a .mergify.yml with your queue rules — most teams start with a simple “queue any PR with N approvals” rule and refine from there

3

Turn off GitHub’s queue on that repo

4

Watch the first 20 PRs go through

Most teams do this on a low-traffic repo first, run both queues side by side for two weeks, then migrate the rest once they have data. The comparison page has more on the feature gap and a few customer stories from teams that made the switch.

The honest framing

GitHub’s merge queue is good. It is also the floor, not the ceiling. If you are hitting the limits above and asking yourself whether you have outgrown it, you probably have. The cost of switching is one engineer-day per repo. The cost of not switching is a queue that gets slower every quarter and a CI bill that keeps climbing.

tested = merged
the exact SHA you tested is the SHA that ships
60–80%
drop in queue CI minutes after batching
1 day
to move one repo off GitHub’s queue onto Mergify

The signal we hear most often from teams that switched: “I wish we had done this six months earlier.”

Merge Queue

Tired of broken main branches?

Mergify's merge queue tests every PR against the latest main before merging. Try it free.

Learn about Merge Queue

Recommended posts

Merge Queue & CI

Path Filters Are a Convenience, Not a CI Gate

June 26, 2026 · 8 min read

Path Filters Are a Convenience, Not a CI Gate

Skipping CI with paths: is a fine optimization until you make the check required. Then it breaks, and not because the glob is imprecise. A path-filtered workflow that doesn't run reports no status at all, so the required check sits pending and blocks the merge, or you fake it green and gate on nothing. Here's why a path filter can't be a merge gate, and what to use instead.

Julien Danjou Julien Danjou
Merge Queue & CI

Stop Using Labels to Control CI in GitHub Actions

June 26, 2026 · 9 min read

Stop Using Labels to Control CI in GitHub Actions

Gating CI on a PR label feels clean until you learn how GitHub fires label events. Any label change re-runs the whole workflow from scratch, and a required check that's skipped when the label is absent counts as a pass, so PRs merge without ever running it. Here's why the pattern breaks and what to do instead.

Julien Danjou Julien Danjou
When a production deploy fails, our merge queue freezes itself
June 24, 2026 · 7 min read

When a production deploy fails, our merge queue freezes itself

A failed production deploy used to be invisible to our merge queue, which kept merging onto a broken release. Here's the 66-line GitHub Actions job that now freezes the queue the moment any deploy scope fails, and why lifting it is a human's job.

Thomas Berdy Thomas Berdy