Short answer: aim for around 100–200 changed lines of real logic per pull request, treat 400 as the point where review quality drops, and treat 1,000 as too large. Size is the single biggest lever on how fast a PR gets reviewed, and the one authors control completely.
What the guidance says
- Google’s engineering practices: “100 lines is usually a reasonable size for a CL, and 1000 lines is usually too large.” They add that a change spread across many files is bigger than its line count suggests.
- SmartBear’s code review research (based on a study at Cisco): review no more than 200 to 400 lines at a time, at under 500 lines per hour, for no more than 60 minutes. Beyond those limits, reviewers find a smaller share of the defects.
The two agree on the shape: small changes get reviewed sooner and more carefully. Google’s reason is practical: it is “easier for a reviewer to find five minutes several times” than a 30-minute block.
What real PRs show
We measured time to first human review against PR size on the last
100 merged PRs in facebook/react (Oct 6, 2026), using
our free script:
| Lines changed | PRs | Median wait | p75 wait |
|---|---|---|---|
| 0–50 | 29 | 6.9h | 18.4h |
| 51–200 | 45 | 3.5h | 24.8h |
| 201–400 | 13 | 3.7h | 12.6h |
| 401–1,000 | 7 | 19.7h | 22.1h |
| 1,001+ | 5 | 44.4h | 2.8d |
Up to about 400 lines, size does not predict how fast React’s PRs get picked up; the smallest PRs actually waited a little longer, and all 87 PRs up to 400 lines together had a median wait of 4.5 hours. Past 400, the median wait jumps roughly fourfold, and past 1,000 it is nearly two days. The large buckets are small samples, so treat the exact numbers loosely, but the cliff lines up with the guidance above. Run the script on your own repo; your cliff may sit somewhere else.
Why big PRs wait
- They need a calendar slot. A 50-line PR gets reviewed between meetings. A 1,500-line PR needs an uninterrupted hour, so it waits for one.
- They get worse reviews. Past a few hundred lines, reviewers skim. Big PRs attract “LGTM” and small ones get real comments.
- They need more rounds. More code means more comments, more fixes, and more re-reviews, each one another wait.
- They conflict. A branch that lives for a week collides with everything merged that week.
How to split a big change
Separate refactoring from behavior
The most common bloated PR is a feature plus the cleanup needed to build it. Ship the rename, move or extraction first as its own PR with no behavior change. It reviews in minutes because the reviewer only has to confirm nothing changed.
Stack dependent PRs
Google calls this stacking: send one small change for review, then immediately start the next one on top of it. Reviewers see a chain of small diffs instead of one huge one, and you are never blocked waiting.
Split by layer
Schema and migration, then the API, then the UI. Each layer can be reviewed by the person who knows it best.
Ship dark behind a flag
Merge incomplete work behind a feature flag that is off in production. You can merge small pieces daily without users seeing a half-built feature.
Keep generated files out of the count
Lockfiles, snapshots and generated code inflate line counts without
adding review work. Mark them as generated in
.gitattributes (package-lock.json
linguist-generated=true) so GitHub collapses them in the diff.
Make size visible: a size-label Action
Nobody remembers a size limit. A label on every PR makes size
visible without blocking anyone. Save this as
.github/workflows/pr-size.yml:
name: PR size
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
issues: write # creating labels
pull-requests: write # labeling the PR
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Label by lines changed
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
ADDED: ${{ github.event.pull_request.additions }}
DELETED: ${{ github.event.pull_request.deletions }}
run: |
lines=$((ADDED + DELETED))
if [ "$lines" -le 50 ]; then size=XS
elif [ "$lines" -le 200 ]; then size=S
elif [ "$lines" -le 400 ]; then size=M
elif [ "$lines" -le 1000 ]; then size=L
else size=XL; fi
gh label create "size/$size" -R "$REPO" --force >/dev/null
for s in XS S M L XL; do
if [ "$s" != "$size" ]; then
gh pr edit "$PR" -R "$REPO" --remove-label "size/$s" 2>/dev/null || true
fi
done
gh pr edit "$PR" -R "$REPO" --add-label "size/$size"
It counts generated files too, so a lockfile bump can land in
size/L. Start with labels, not a failing check: a hard
limit pushes people to split PRs in ways that make them harder to
review. After a month, look at how long size/XL PRs
waited compared with size/S; that comparison persuades
a team better than any rule.
Forks: PRs from forks get a read-only token, so the label step fails
there. For open-source repos, run it on
pull_request_target instead, which is safe here because
the job never checks out the PR’s code.
When a big PR is fine
- Mechanical changes: a rename across the codebase, a formatter run
- Generated code, vendored dependencies, or data files
- Deleting dead code (deletions are cheap to review)
Say so in the description (“mechanical rename, no logic change”) so the reviewer knows how to read it. Everything else: if it is over 400 lines, ask whether it is really one change. For more author-side tactics, see how to get pull requests reviewed faster.