Dev.to AI 🤖 Ai 👁 0 📖 7 min read

How Many Pull Requests a Merge Queue Can Land in an Hour

Engineer's view. This is optimistic locking under contention. Each transaction validates against everything committed before it, and one abort throws away the work queued behind it. More workers only waste more work. Thr

How Many Pull Requests a Merge Queue Can Land in an Hour

Engineer's view. This is optimistic locking under contention. Each transaction validates against everything committed before it, and one abort throws away the work queued behind it. More workers only waste more work. Throughput is set by the abort rate and the transaction time. Agents raise the load and leave that ceiling where it was.

TL;DR. A merge queue's speed is capped by how often a queued change fails and how long CI takes, not by how many pipelines run at once. More pull requests from agents push it into long waits unless you cut one of those two.

Explain Like I'm 5

A school lunch line has one rule. Each kid must show their tray to the cook, and the cook
checks it against every tray that went before.

The cook can look at twenty trays at once. If a tray is wrong, that kid steps out. Every kid
behind them has to show their tray again, because the cook checked it next to the wrong one.

On a normal day the line moves fine. Then a class of fast kids joins. They bring three times
as many trays. The cook is not slower. The line still backs up, because each wrong tray sends
everyone behind it back to the start.

The Problem

You have shipped this bug already, in a system with no model in it.

Your service used optimistic locking. Each write read a row, changed it and committed only if
nobody else had changed it first. At low traffic it was fast. Then traffic tripled. Writes
started aborting and retrying, and each retry raced the next wave. Latency went from
milliseconds to seconds. Adding app servers made it worse, because they only added contenders.

The limit was never the servers. It was how often a commit failed, times how long it took.

A merge queue has the same shape. It is the gate that lands pull requests on main one
tested batch at a time. GitHub calls it the merge queue. GitLab calls it a merge train. Each
pull request is tested on top of main plus every pull request ahead of it, so main never
breaks. When one fails, everything behind it was tested on a tree that included the failure.
All of it reruns.

Coding agents change the arrival rate. At Anthropic, merges grew from about 2,500 a day in May
2026 to over 30,000 a day by September. They stopped using a merge queue. A team without that
option has to know where its queue's ceiling is.

The fix: measure your queue's failure rate and CI time, compute its ceiling, and lower the
failure rate before adding pipelines.

The Fix: Cut the Failure Rate Before You Add Pipelines

Four pull requests in a queue, four pipelines running

Why doesn't running more pipelines fix it?

Call the share of queued pull requests that fail p, and the CI time T. Between two
failures the queue lands about 1/p pull requests. Each failure throws away up to T of work
behind it. So no number of pipelines lands more than about 1/(p × T) an hour.

With a 30-minute CI and 5% failing, that is 40 an hour. With 20 pipelines it is about 24. Going
to 100 pipelines buys 38, at five times the CI bill.

Where does the failure rate come from?

Two places. Real breaks: a change that passed on its own branch and conflicts with one ahead of
it. And flaky tests, tests that fail now and then with no change at all. Flakes add up. Fifty
tests that each flake one run in a thousand fail about one run in twenty. That is p = 5%.

What happens to the wait near the ceiling?

It climbs fast. A queue at 8 pull requests an hour waits 34 minutes on average. Triple the
arrivals to 24, inside the ceiling on paper, and the wait passes 100 minutes.

Mean wait in the queue at 24 pull requests an hour, 20 pipelines

What This Means for You

When this matters. Your repo uses a merge queue, and people or agents open more pull
requests than they did last quarter. It matters most when CI runs long and has a few tests
everyone retries. If you see "removed from the merge queue" often, you are paying for it.

How it affects you. The queue feels slow, and the obvious fix is more parallel pipelines.
That buys little once failures are common, and it raises the CI bill. Waits grow faster than
volume. Agents then rebase on a stale main and conflict with each other, which adds real
failures on top of the flaky ones. A flaky test used to be an annoyance. Under load it sets
your throughput.

What to do about it. Start with two numbers from last week, with no new tooling. Count the
pull requests the queue merged, and the ones it removed for failed checks. Divide removed by
the total, and that is p. Read T off any recent queue run. Then compute 1/(p × T) and compare it
with your busiest hour.

If the busy hour is within half of the ceiling, waits are already long. Lower p first: retry a
failing test once before ejecting, and quarantine the tests that flake. Then shorten T by moving
slow checks out of the queue. Add pipelines last.

Implementing It

The change.

Four people touch this. Each change is small, and they compound.

Role 1, whoever owns CI. Run only fast required checks on the merge group, and make a flake
fail twice before it ejects a pull request. In GitHub Actions the queue fires the merge_group
event. The retry here uses the pytest-rerunfailures plugin.

on:
  pull_request:
  merge_group:
jobs:
  required:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest -q --reruns 1   # a flaky test must fail twice to eject

On GitLab, the same idea is retry: { max: 1, when: script_failure } on the job. A retry turns
a 5% flake rate into about 0.25%, since both runs have to fail. It also hides flakes, so pair it
with Role 3.

Role 2, whoever administers the repo. Size the queue from the formula, not by feel. GitHub
sets the pipeline count with build concurrency (1 to 100). GitLab allows 20 parallel
pipelines by default.

def landed_per_hour(depth, p, ci_minutes):
    return 60 * depth / (ci_minutes * (1 + p * depth))

# depth 1/p reaches half the ceiling; past about 3/p you mostly pay for reruns

Plug in your own numbers before you touch the setting. With p at 5% and a 30-minute CI, twenty
pipelines land about 20 an hour by the formula and 24 in the simulation. Cut p to 1% and the same
twenty land about 33. That is the cheaper change.

Role 3, whoever owns the test suite. Find the flakes. A test that fails and then passes on
the same commit is flaky. Log those, rank tests by count, and quarantine the top of the list
until they are fixed.

flaky_runs = 1 - (1 - 0.001) ** 50     # fifty tests at 0.1% each -> about 5% of runs

Role 4, whoever runs the agents. Run the queue's checks before a pull request enters it.
Rebase on fresh main first. A conflict caught on the branch costs one pipeline, while one
caught in the queue costs every pipeline behind it.

How you know it worked.

Recompute p and T a week later. The removed-for-failure count should fall first. Mean time in
the queue should drop with it, and landings per busy hour should come within reach of
landed_per_hour. If p falls and the wait does not, T is now the limit. Look at which check is
slowest.

When Tuning the Queue Is the Wrong Tool

When volume is low. A team landing a few pull requests an hour is far below any ceiling. Waits
come from CI time alone, so shorten CI and leave the queue alone.

When failures are real, not flaky. If most ejected pull requests were broken, retries only delay
the same answer. Catch them earlier, on the branch, which is Role 4's job.

When volume outgrows any queue. Anthropic went from about 2,500 merges a day to over 30,000 and
moved most testing to after the merge and before deploy. That trades a green main for speed.
It needs fast reverts and someone, or something, that fixes a broken main within minutes. It
is a bigger change than tuning a queue.

Batching has a cost too. Larger merge groups test several pull requests in one pipeline. A
failure then has to be split to find the culprit, which costs more runs.

Three questions before you change anything:

  • What share of last week's queued pull requests were removed for failed checks?
  • How many minutes do the required checks take on a merge group?
  • How close is your busiest hour to 1/(p × T)?

Glossary

  • merge queue — the gate that tests pull requests on top of each other and lands them on main in order.
  • merge group — the temporary branch a queue builds: main plus a pull request and everything ahead of it.
  • flaky tests — tests that fail some runs and pass others on the same code.
  • build concurrency — GitHub's setting for how many merge group pipelines run at once.
  • p — the share of queued pull requests that fail their checks, from real breaks or flakes.

Further reading

Originally published at theaicommit.com, a daily AI engineering lab for software engineers. The original has an interactive version you can play with, and the full runnable code is on GitHub.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.