Live: https://f1bonacc1.github.io/prs-throughput/
An interactive model of how many pull requests a team can actually land on main
each day, once CI validation is a contended resource — and which constraint is
really holding it back.
Everything is in a single self-contained index.html. No build step, no
dependencies, no network calls. Open the file locally and it works.
Reruns forced by rebasing scale as merges/day × open PRs, so CI demand grows
with the square of team throughput while the VM pool stays flat. Past a
certain team size, adding developers reduces merged PRs per day.
With a strict "branch must be up to date" rule at the default settings:
| Developers | 10 | 16 | 25 | 40 |
|---|---|---|---|---|
| PRs merged/day | 12.5 | 8.7 | 6.7 | 5.0 |
Forty developers land fewer PRs per day than ten. A merge queue removes the cliff entirely, because it validates a batch once instead of forcing every open PR to revalidate.
- Team — developers, PRs opened per day, open PRs per developer, fix commits per PR
- Validation — workflow duration, VMs per workflow, VMs available, flaky failure rate, VM cost
- Merge process — rebase policy (strict / overlap-only / merge queue), batch size, review wait, time to write a fix, working hours per day
A closed-loop queueing model. Throughput is the lowest of three ceilings, solved together with the number of open PRs because each depends on the others:
demand ceiling = PRs the team opens per day
WIP ceiling = open PR slots / PR cycle time
CI ceiling = pool throughput / runs consumed per merged PR
merged/day = min(the three)
Queueing uses Erlang C on the daily average, plus a fluid approximation for the backlog that builds during working hours and drains overnight. The solver takes the lowest equilibrium: strict rebasing is positive feedback, so the system has several self-consistent operating points, and a team fills from empty rather than jumping into congestion collapse.
The in-page "Model, formulas, and what this does not capture" panel shows the live formulas with current values substituted, and states the model's limits.
The engine is delimited by /*==ENGINE_START==*/ … /*==ENGINE_END==*/ inside
index.html and is pure (no DOM access), so it can be extracted and unit-tested
directly from the shipped file.