Merge Queue Chaos: Why AI-Generated PRs Overwhelm Traditional CI/CD Pipelines
The Short Version
A merge queue is built around an assumption: pull requests arrive at roughly the pace humans write code, giving the queue time to serialize, re-test, and land each one before the next arrives. AI coding agents break that assumption directly. They open PRs faster, revise them faster, and push follow-up commits faster than the queue's throughput was ever tuned for. The queue does not fail loudly. It just gets slower, and slower, until someone notices a PR sat in queue for three hours waiting for a test run that used to take fifteen minutes.
What a Merge Queue Actually Does
Before getting into what breaks, it helps to be precise about the job a merge queue does, since it is easy to conflate with plain CI.
A pull request's own CI run tests that PR against the current state of main at the moment it was opened. If two PRs, each individually passing, get merged one after another, nothing has re-tested them together against each other's changes. A merge queue closes this gap: it takes PRs that are ready to merge, serializes them, and re-runs tests against each one layered on top of the previous one, so main never receives a combination nobody actually tested.
This is a genuinely valuable guarantee. It is also, by construction, a serialization point, and serialization points have throughput limits.
Where AI Agents Actually Break the Model
The queue's throughput was sized around a specific cadence: a handful of engineers, each opening a few PRs a day, each PR taking meaningful human time to write and revise. Three things change when AI agents enter the picture, and each one independently strains the queue.
More PRs enter the queue. An agent that can implement a small feature or fix in minutes produces PRs at a rate no individual human matched. Multiply that by several engineers each running an agent, and queue entries per hour can jump by a large multiple with no change in team size.
Each PR gets revised more times before it is queue-ready. Agents iterate quickly on review feedback, often pushing several follow-up commits within the same review cycle. Every push that re-enters the queue consumes another serialized test run.
The queue's own re-test step becomes the shared bottleneck. This is the part that actually causes chaos, not just volume. If the queue's test step depends on a single shared staging environment or a fixed pool of test infrastructure, adding more queue entries does not parallelize, it queues up behind the same limited resource. The merge queue was supposed to be the orderly part of the pipeline; instead it becomes the visible symptom of a deeper capacity problem.
Why "Just Add More CI Runners" Doesn't Fix This
A common first response to a slowing queue is to add more parallel CI runner capacity, on the assumption that the bottleneck is compute. This helps when the constraint genuinely is compute, more parallel test execution slots processing more entries at once, but it does nothing when the constraint is a single shared deployment target that every entry's test step depends on regardless of how many CI runners are available to orchestrate that dependency.
The distinction matters because the two fixes look similar on a budget line (both are "spend more on infrastructure") but solve completely different problems. More CI runners fixes a compute-bound bottleneck. More runners pointed at the same one shared staging environment just means more queue entries arrive at the front of the line faster, waiting on the same single resource, which can actually make the visible symptom worse, not better, since entries now queue up at the environment step instead of being spread out by the CI scheduler.
Before spending on more CI capacity, it is worth confirming directly whether CI compute or environment availability is the actual constraint, by checking whether queue entries spend their wait time queued for a runner or queued for the shared environment to free up. If it's the latter, no amount of additional CI runner budget resolves it.
The Failure Mode, Concretely
Here is what this actually looks like day to day, since "the queue is slow" undersells how disruptive it gets.
A queue entry that used to clear in fifteen minutes now waits for a test slot that's occupied by three other entries ahead of it. A developer (or their agent) sees the queue stalled, assumes something is broken, and force-pushes an unrelated fix, which re-enters the queue at the back, extending everyone's wait further. By the time queue entries are measured in hours instead of minutes, engineers start batching multiple changes into fewer PRs specifically to avoid the queue, which defeats the small-PR discipline the queue was supposed to encourage in the first place.
None of this is a bug in the merge queue software. It is the queue correctly doing its job under load it was never sized for.
The Fix Is at the Environment Layer, Not the Queue
The instinct is to make the queue bigger or faster: more parallel queue lanes, a faster CI runner, a shorter test suite. These help at the margins, but they do not address the actual constraint if the underlying test step depends on shared, contended infrastructure.
The structural fix is removing that dependency: give every queue entry (and every PR feeding into it) its own full-stack, isolated environment, so testing one entry never waits on another entry's environment being free.
1$ bns environments create --from-path bunnyshell.yaml \
2 --name "queue-entry-482" --project <PROJECT_ID> --k8s <CLUSTER_ID> --deploy
3
4Environment successfully created... deploying...Each queue entry gets a real, independently provisioned environment rather than a reservation on one shared staging slot. The merge queue still serializes the merge decision, which is its actual job, but it no longer serializes on the test infrastructure, which was never supposed to be its job.
Sizing Environment Capacity for Queue Throughput
Once environments are no longer shared, the practical question becomes cluster capacity: how many concurrent queue-entry environments can your cluster actually run.
$ bns environments list --project <PROJECT_ID> -o json | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(len([e for e in d if e.get('operationStatus')=='running']))"Tracking this number over a week of real queue activity gives you an honest peak-concurrency figure to plan node-pool autoscaling around, rather than guessing. A queue that regularly peaks at eight concurrent entries needs a cluster (or autoscaler ceiling) sized for eight full environments running simultaneously, not for the one-or-two-at-a-time pattern a pre-AI team's queue used to produce.
Auto-Destroy Keeps the Math From Compounding
The other half of this equation, easy to overlook, is teardown discipline. A queue processing many more entries per day also produces many more environments that need to disappear once their entry clears or gets superseded.
bns environments update-settings --id <PRIMARY_ENV_ID> \
--destroy-ephemeral-on-pr-closeWithout reliable auto-destroy, a high-throughput queue quietly accumulates running environments at a rate proportional to the new PR volume, which turns a solved contention problem into a new cost problem within a few weeks. See Auto-Destroy After Merge for the exact mechanism and the failure modes it closes.
Does This Mean the Merge Queue Becomes Unnecessary?
No, and it's worth being precise about why. The merge queue's actual job, catching integration conflicts between individually-passing PRs, does not go away just because environments stop being contended. Two AI-generated PRs can still combine into a broken main even when each one tested cleanly in isolation. What changes is that the queue is no longer also silently functioning as a rationing mechanism for scarce test infrastructure. Once it only has to do its actual job, its real throughput ceiling goes up substantially, since re-testing a queue entry against an available, isolated environment takes the same fifteen minutes it always did, instead of fifteen minutes plus however long the wait for a shared slot happened to be.
A Practical Migration Path
Teams discovering this the hard way, mid-sprint, with a backed-up queue, do not need to redesign their whole pipeline at once.
- Confirm the queue's test step is actually the contention point, not something else (check queue wait time versus actual test-run duration; if wait time dominates, this is it).
- Wire the queue's test step to provision a fresh environment per entry instead of reusing one shared instance. See Automated PR Environments for E2E Testing for the create-deploy-test-destroy pattern this depends on.
- Set a realistic node-pool ceiling based on your actual measured peak concurrency, not a guess.
- Confirm auto-destroy is enabled before, not after, queue volume ramps further.
What This Looks Like Across Different Merge Queue Tools
The specific mechanism differs by tool, but the underlying capacity problem is identical, so it's worth recognizing the pattern regardless of which queue implementation a team runs.
GitHub's native merge queue serializes entries and re-runs required status checks against each one merged on top of the previous. If those status checks deploy to a shared environment, the queue inherits that environment's contention directly, and GitHub's own queue concurrency settings cannot fix a bottleneck sitting one layer below the queue itself.
Mergify and similar third-party merge-queue tools add scheduling and batching logic on top of the same underlying constraint. Batching several PRs into one combined test run reduces the number of serialized test executions, which helps, but it does not remove the shared-environment dependency if the batch itself still deploys to one contended instance.
Bors-style "merge only if CI passes against the merge commit" bots hit this hardest of all, since they by design re-test every single queue entry individually against the latest main, with no batching to soften the load. A high-throughput bors queue against a single shared staging environment is close to a worst-case scenario for this specific failure mode.
The fix, giving every queue entry its own environment, applies identically across all three, since none of them actually require a shared environment to do their job. They require a consistent, isolated environment per test run, which per-PR ephemeral environments provide more cleanly than a shared instance ever could.
A Second-Order Effect: Flaky Tests Get Blamed on the Wrong Thing
A subtler failure mode worth naming directly: when a shared staging environment is under contention, test failures caused by environment state (leftover data from a previous queue entry, a stuck process, a partial deploy that hadn't finished when the next entry's tests started) get misdiagnosed as flaky tests. Someone adds a retry, or worse, marks the test as "known flaky" and starts ignoring its failures.
This compounds badly with AI-generated PR volume specifically, since higher throughput means more queue entries landing close together in time, which increases the odds of exactly this kind of state bleed between one entry's teardown and the next entry's setup on a shared instance. Teams sometimes spend real engineering time hardening tests against flakiness that was never actually about the tests at all, it was about environment contention the test suite had no way to see or report accurately.
Isolating environments per queue entry removes this failure mode by construction, since there is no shared state left to bleed between runs. A test that fails after this change either has a real bug or a real flakiness issue, not a phantom one caused by infrastructure sharing.
Measuring Whether the Fix Actually Worked
After moving to per-entry environments, it's worth confirming the change delivered what it promised, rather than assuming. Two numbers matter.
Average queue wait time before test results are available. This should drop substantially and, more importantly, should stop growing as PR volume grows, since contention was the variable driving that growth before.
1bns pipeline list --environment <PRIMARY_ENV_ID> -o json | \
2 python3 -c "
3import json, sys
4from datetime import datetime
5data = json.load(sys.stdin)
6durations = [(datetime.fromisoformat(p['finishedAt']) - datetime.fromisoformat(p['createdAt'])).total_seconds() for p in data if p.get('finishedAt')]
7print(f'avg pipeline duration: {sum(durations)/len(durations)/60:.1f} min' if durations else 'no data')
8"Flaky-test report rate. If a meaningful share of previously-flagged flaky tests stop failing intermittently once environments are isolated, that's direct confirmation the earlier flakiness was environment contention, not the tests themselves, and it's worth revisiting any test that was previously silenced or given a retry as a workaround.
Related Reading
For the underlying math behind exactly why wait time explodes rather than growing gradually as arrival rate climbs, see Staging Environment Capacity Math, which applies the same queuing model this guide describes to a merge queue's re-test step specifically. For the team-wide infrastructure changes this fix requires beyond the queue itself, see Scaling Staging Infrastructure for AI Teams.
Checklist
- Confirmed the merge queue's bottleneck is shared test infrastructure, not the queue mechanism itself
- Every queue entry gets its own environment instead of a reserved slot on shared staging
- Cluster capacity is sized against measured peak concurrent environments, not assumption
- Auto-destroy is enabled so higher throughput does not compound into forgotten environments
Stop letting your merge queue ration test environments.
14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.
Frequently Asked Questions
What is a merge queue, and why does it exist?
A merge queue serializes pull requests before they land on the main branch, re-testing each one against the latest state of main so that two individually passing PRs cannot combine into a broken main branch. It exists to catch integration conflicts that per-PR CI alone misses.
Why do AI coding agents break merge queue assumptions specifically?
Merge queues were sized and tuned around how fast humans open and update pull requests. An agent that can open, revise, and re-push a PR in minutes instead of hours multiplies queue entries and re-test cycles far faster than the queue's throughput was designed to absorb, so the queue itself becomes the bottleneck.
Does a bigger or faster merge queue solve this?
It helps, but it treats a symptom. The queue re-runs the same test suite against the same limited pool of test infrastructure for every entry; if that infrastructure is a shared staging environment, adding queue capacity does not remove the underlying contention, it just reshuffles who waits.
Is the fix to slow down the AI agents instead?
That is the wrong lever. The actual bottleneck is test infrastructure capacity, not the rate of code generation. Giving every PR its own full-stack environment removes the shared-resource constraint the merge queue is fighting, which lets the queue do the one job it is actually meant for: serializing merges safely, not rationing test environments.
Does this only affect large engineering organizations?
No. A small team of five developers using an AI coding assistant can generate PR volume that used to take a team of twenty, so this shows up earlier than most teams expect, often the first time someone notices the CI queue backing up for an hour during a normal sprint.