Stale pull requests: why they happen and how to stop them

A stale pull request is one that is open, nobody is touching it, and nobody feels it is theirs to move. In our experience it's a nuisance in a team of five and a weekly tax in a team of twenty.

The usual reflex is to say the team needs someone whose job is to chase this. Our view is different: most stale PRs come from a few structural gaps you can close yourself, with tools you already have. In our opinion, hiring a manager to watch the review queue is an expensive way to fix a CODEOWNERS file.

Why pull requests stall

Nobody is clearly the reviewer

If a PR goes up and no one is requested, everyone can see it and nobody has to act. GitHub can request reviewers for you automatically, but only if you tell it who owns what. Without that, the author picks someone, or picks nobody, and hopes.

The PR is too big

Big changes are harder to start reviewing. Google's engineering practices guide says small changes are "reviewed more quickly" because it is easier to find five minutes several times than to block out 30 minutes for one large change. The same guide puts about 100 lines as usually reasonable and 1,000 lines as usually too large, while leaving the final call to the reviewer. It also notes that large changes produce frustrating volumes of back-and-forth commentary, and each round trip is another chance to stall.

Ownership of the code itself is fuzzy

When nobody knows who "has" the billing module, review requests land on whoever is most senior or most polite, and they become the bottleneck. In our experience this is the most common cause of stale PRs in teams of 5 to 30: not laziness, just a missing answer to "whose call is this?"

Everyone is waiting for someone else

A PR has at least two parties: the author and the reviewer. In our experience, each tends to assume the ball is in the other court. Microsoft researchers who studied overdue pull requests looked at 200 PRs that had been open for at least 14 days and found that 130 were waiting on the author and 70 on reviewers. That was at Microsoft scale, so treat it as a hint rather than a law for your team. Still, it's a useful corrective: a stale PR is not always the reviewer's fault. Often the author has comments to address, or an approval they never merged.

How to fix it

None of these fixes needs a new hire. Pick the two or three that match your bottleneck.

1. Add a CODEOWNERS file

A CODEOWNERS file tells GitHub who owns which paths. According to GitHub's documentation, code owners are automatically requested for review when someone opens a pull request that changes code they own.

How to set it up:

  1. Create a file called CODEOWNERS in .github/, the repository root, or docs/. GitHub looks in that order and uses the first one it finds.

  2. Add one line per pattern, followed by one or more users or teams:

    # Default owners for everything
    *                 @your-org/core
    
    # More specific rules go lower; the last match wins
    /billing/         @your-org/payments
    /infra/           @alice @bob
    *.md              @your-org/docs
  3. Commit it to the branch your PRs target. Review requests use the CODEOWNERS file on the PR's base branch.

  4. Check that every owner has write access. GitHub's docs say users and teams need explicit write access, and an owner without enough access is simply not assigned.

  5. Open the file on GitHub to see syntax errors. Invalid lines are skipped, not rejected.

Code owners aren't requested on draft PRs until they're marked ready.

If you use teams as owners, GitHub can also auto-assign individual reviewers from the team using round robin or load balancing, and it skips members who set their status to "Busy." That turns "the team was requested" into "a named person was requested," which matters a lot.

2. Require the review, but keep it light

Branch protection can require a number of approving reviews before merging, and can require approval from a code owner specifically.

To turn it on: go to the repository's Settings, open branch protection rules (or rulesets), add a rule for your default branch, and enable Require pull request reviews before merging. Optionally enable Require review from Code Owners.

A caution: if a path has only one owner, that person becomes a single point of failure. Add a second; GitHub accepts an approval from any one listed owner.

DORA's research finds change approval works best as peer review during development, backed by automation, and that heavyweight external approval hurts delivery performance. Required and fast, not ceremonial.

3. Make PRs small on purpose

Small PRs are a team habit, not a tool setting. A few ways to make them normal:

4. Agree on a review SLA

A review SLA is a team agreement about how fast someone responds to a review request. It doesn't have to mean finishing the review. Google's guidance is a good starting point: one business day is the maximum time to respond to a review request, and the response matters more than the total time to merge. The same guide says not to break your own focus to do a review. Wait for a natural break.

How to adopt one:

  1. Write one sentence in your contributing guide: "First response on a review request within one working day."

  2. Define "response." A review, "will look tomorrow," or "ask @bob instead" all count.

  3. Say what the author owes too: replying to comments and merging once approved. Given the Microsoft finding above, this half is easy to forget.

  4. Check it weekly (see the digest below) instead of policing it in real time.

Our opinion: a soft norm that people can see works better than a hard rule nobody checks.

5. Use GitHub's scheduled reminders

GitHub has a built-in feature for exactly this. Scheduled reminders for teams send your team a message in Slack at set times listing open PRs the team has been asked to review. Note the requirement: an organization owner has to authorize your Slack workspace first.

How to set it up:

  1. Go to your organization, then Teams, pick the team, and open Settings.

  2. Under Integrations, choose Scheduled reminders, then add a reminder.

  3. Authorize your Slack workspace and pick a channel.

  4. Choose days and times, for example weekdays at 9:00.

  5. Pick the tracked repositories.

  6. Set Minimum age and Minimum staleness (in hours) so brand-new or recently active PRs are left out.

  7. Optionally ignore drafts and approved PRs.

Each reminder covers up to five repositories and up to 20 of the oldest PRs per repository; add more reminders for more coverage.

6. Send a weekly digest

As we see it, daily reminders handle "please review this." They don't answer the question a founder-CTO actually has on Friday: what's stuck, why, and whose move is it?

In our opinion, a weekly written digest fills that gap. You can build one by hand with GitHub search. GitHub's search qualifiers cover most of it:

Add org:your-org or repo:owner/name to scope it. For each stale item, write one line: what it is, who has the next move, and what you'd like to happen. We have a weekly engineering status report template you can copy.

7. Nudge stale PRs, and do it carefully

A nudge is a direct reminder on a specific overdue PR, aimed at the person who needs to act. There is published evidence that this works. In a randomized trial across 147 repositories at Microsoft, overdue PRs that got a nudge closed much faster than overdue PRs that didn't: average lifetime fell from 197.2 hours to 77.7 hours, and 73% of the notifications were marked positive by developers. The authors point out that this was in Microsoft's environment and has not been verified in other organizations.

The same paper has a useful design lesson. In an early version, 86 of 119 negatively rated nudges were complaints that the tool had ignored recent activity on the PR. The better version only nudged after no activity for 24 hours, aimed the nudge at whoever was actually blocking (author or reviewer), and sent at most one nudge per PR.

Copy those rules, by hand or with a tool:

What not to use for this: the actions/stale action is built to label and then close inactive issues and PRs (by default, labelled after 60 days and closed 7 days later). That's useful for cleaning up a backlog. It's not a way to get a PR reviewed.

Where Pilotless fits

Pilotless is one option for the digest part. Today it's a free GitHub App that sends a weekly status digest from your repositories. The weekly digest lists stalled PRs and unassigned issues. Nudging stale PRs and flagging issues with no owner in their own threads are coming, but aren't available yet. It does not replace CODEOWNERS, branch protection, or a team agreement on review speed. Those are the foundation, and they're free in GitHub already. If GitHub's scheduled reminders already cover you, keep using them.

Where we think it helps: you've done the setup above and want a weekly view of what moved without someone running searches every Friday. For nudges today, use GitHub's scheduled reminders or the manual searches above.

The short version

CODEOWNERS so every PR gets a named reviewer. One required review, with two owners per path. Small PRs. First response within a working day. GitHub's reminders if you're on Slack. A weekly digest. One well-aimed nudge per overdue PR.

Our take: you don't need a manager to do any of this. You need the gaps to be visible.

Install the GitHub App


Sources