Skip to content

Predict Which Pull Requests Will Stall — Using Your Own Velocity Baseline

Aliaume Caplat Founder, DevPrism LinkedIn
pull-requests engineering-metrics dora engineering-management

Every team eventually adds the same automation: “alert me when a pull request has been open for more than 48 hours.”

And every team eventually turns it off.

Not because stale PRs stopped mattering. Because 48 hours is somebody else’s number. On a team that reviews within two hours, it fires far too late to be useful. On a team that reviews twice a week, it fires on everything, every day, until people filter the channel.

A staleness threshold has to come from somewhere. The mistake is picking it by hand.

The Shift: Compare a PR to Your Team, Not to a Constant

The question “is this PR late?” has no absolute answer. It only has a relative one: late compared to what this team normally does.

That reframing turns an opinion into a computation. You already have the data — every merged pull request is one observation of how long review actually takes on your team. Turn those observations into a distribution, and the threshold picks itself.

Two measurements matter, and they are not the same thing:

Measure Definition What it tells you
Pickup time PR opened → first review How long code waits for a human
Cycle time PR opened → closed / merged The whole journey, author’s work included

Pickup time is the one worth alerting on. Cycle time includes the author responding to comments, rebasing, waiting on CI — legitimate work in progress. Pickup time measures something else entirely: nobody has looked at this yet. That is queue time, it is pure waste, and unlike cycle time it is actionable by someone other than the author.

Step 1 — Build the Baseline

Take your merged pull requests over a rolling window and compute the 50th, 75th and 90th percentile of pickup time.

That one sentence hides four decisions, and each one is the difference between a signal and noise.

Use percentile_disc, not percentile_cont

percentile_cont interpolates between two observations. On a sample of twelve pull requests, that invents a duration that never happened — a P75 of “26.4 hours” when your team has never once taken 26.4 hours. percentile_disc returns a real observation. When you later tell an engineer “this exceeds your team’s P75”, you want that number to be a thing that actually occurred.

Exclude maintenance bots

Dependency-update bots open a lot of pull requests and abandon a lot of them. Left in the sample they dominate the distribution, and they distort the human signal you are trying to measure. Picture a dependency bot that opens a few version bumps every week, most of which are auto-merged, superseded by the next bump, or quietly closed. Its durations describe a process no human ever performed — and because it opens far more PRs than any individual, a single bot account can outweigh your entire team in the sample. Nobody was ever going to review those. They are not review debt.

Refuse to answer below a minimum sample

Below roughly ten merged PRs in the window, percentiles are noise dressed as precision. A P90 computed from four observations is just “the slowest of four”. The correct behaviour is to emit nothing — no baseline, no prediction, no alert.

This is worth insisting on, because it is the opposite of what most dashboards do. A tool that always produces a number teaches people that its numbers mean nothing. Refusing to answer is a feature, and it is the cheapest credibility you will ever buy.

Scope the baseline to the team

A platform team and a mobile team do not share a review culture, a time zone, or a definition of urgent. One global percentile flattens both into a number that describes neither.

The query, if you want to reproduce it

Nothing above requires a tool. If you have your pull request history in a warehouse, the whole baseline is one statement — skip it if you don’t, the rest of the article stands without it.

SELECT
  percentile_disc(0.50) WITHIN GROUP (ORDER BY pickup_hours) AS p50,
  percentile_disc(0.75) WITHIN GROUP (ORDER BY pickup_hours) AS p75,
  percentile_disc(0.90) WITHIN GROUP (ORDER BY pickup_hours) AS p90,
  count(*)                                                   AS sample_size
FROM (
  SELECT EXTRACT(EPOCH FROM (first_review_at - created_at)) / 3600 AS pickup_hours
  FROM pull_requests
  WHERE merged_at BETWEEN :period_start AND :period_end
    AND first_review_at IS NOT NULL
    AND author NOT IN (SELECT login FROM maintenance_bots)
) AS observed;

Step 2 — Score the Open Pull Requests

Now walk the open PRs. For each one, take the elapsed time since it was opened and place it in the distribution:

elapsed < P50            → on track       (emit nothing)
P50 ≤ elapsed < P75      → trending slow
P75 ≤ elapsed < P90      → at risk
elapsed ≥ P90            → predicted blocker

Two rules make this usable in practice.

Skip any PR that already has a first review. Once a human has engaged, the PR is not queued any more — it is in progress. Alerting on it produces exactly the false positive that gets the whole integration muted. This single condition removes most of the noise.

Say nothing about PRs below P50. Roughly half your open PRs are on track by construction. Reporting them adds volume and subtracts attention.

And when you do emit something, name the comparison:

No review after 31.4h — exceeds team P75 (26h). At risk of becoming blocked.

Not “this PR is at risk”. The number the reader is being measured against has to be in the message, or the alert is an assertion of authority rather than an argument. It also makes the alert falsifiable: anyone can go check whether 26 hours is really their P75, and that is the point.

What This Does Not Tell You

A model you can’t criticise is a model you shouldn’t ship. Four honest limits:

The baseline is built on survivors. It only counts pull requests that got reviewed and merged. The ones that were abandoned after rotting for three weeks contribute nothing, so the distribution is systematically more optimistic than your team’s real experience. This makes the alert conservative — when it fires, it means something.

The baseline drifts with the team. A quarter of poor review hygiene raises P75, and a higher P75 makes the alert quieter. The measurement adapts to the decline instead of reporting it. So track the baseline itself over time, as a metric in its own right: a P90 that doubles in two months is the finding, and no per-PR alert will ever show it to you.

It predicts one specific failure. Stalling before first review — that’s all. It says nothing about whether the code is correct, whether the PR is too large, or whether CI is about to fail. It is one signal among several, not a risk score.

Small teams get noisy percentiles. Ten observations is a floor, not a comfort. With a small sample, prefer a longer window over a tighter threshold.

Why This Is Worth the Trouble

A fixed threshold is a policy: someone decided 48 hours. A percentile baseline is a measurement: your team decided, by doing the work.

That distinction matters more than the accuracy gain. An alert that says “this exceeds the number I was configured with” invites an argument about the configuration. An alert that says “this exceeds what your team achieves three times out of four” invites a conversation about the queue — which is the conversation you wanted.

How DevPrism Does It

DevPrism computes this baseline per team on every sync, and scores open pull requests against it:

  • Pickup-time and cycle-time percentiles from your merged PRs, maintenance bots excluded
  • Discrete percentiles, so every threshold is a duration your team has actually recorded
  • Prediction suppressed below the minimum sample, and on PRs already under review
  • Each flagged PR carries the ratio to the baseline and a reason naming the percentile it crossed
Open pull requests scored against the team's own baseline — the flagged ones carry the percentile they crossed, not a generic warning.

The point of all this is not to predict the future. It is to stop asking every engineering manager to invent a number, and to make the alert defensible to the person receiving it.


Want this baseline computed for you, per team, from your own history? Try DevPrism free — full PR Intelligence and risk scoring are part of the Pro plan, included in the 14-day trial, no credit card.